1. 서론
전에 Linux 서버에 WebTerminal로 원격 작업을 할 수 있게 개발을 했는데,
서버 종류에 Window 서버가 추가되었다.
그래서 Window 서버를 GUI를 사용하여 원격으로 작업할 수 있는 방법을 찾아보다 RDP(Remot Desktop Protocol)을 알게되어, 이를 구현하게 되었다.
점진적으로 TypeScript로 바꿀 예정이라, 차후에 변환이 완료되면 본글을 수정하도록 하겠다.
[개발 환경]
Front : NextJS, TypeScript
BackEnd : NestJS, TypeScript
+ RDP로 접속할 Window 서버 하나
2. Front-end
먼저 서버와 통신하기 위해 socket.io-client를 설치한다.
npm i socket.io-client
그리고 프론트 코드의 기본틀은 node-rdpjs를 만든 citronneur님의 틀을 가져왔다.
GitHub - citronneur/mstsc.js: A pure Node.js Microsoft Remote Desktop Protocol (RDP) Client
A pure Node.js Microsoft Remote Desktop Protocol (RDP) Client - citronneur/mstsc.js
github.com
그후 위 프로젝트에서 사용하는 js파일들을 그대로 가져와 필요한 부분들을 수정하면 된다.
먼저 page.tsx를 작성한다.
해당 프로젝트에서는 새창이 열림과 동시에 바로 접속되는 상황이라서, 입력을 받는다거나 다른 프로세스가 필요하다면 수정이 필요할 것이다.
"use client";
import { Token } from "@/interface/Token.interface";
import { getUserInfo } from "@/utiles/common";
import { NEXT_PUBLIC_API_URL } from "@/utiles/constants";
import Script from "next/script";
import { useEffect, useRef } from "react";
import { io } from "socket.io-client";
declare const Mstsc: any; // 전역 객체 타입 선언
export default function RdpPage(args : any) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const clientRef = useRef<any>(null);
let socket = io(NEXT_PUBLIC_API_URL, {transports : ["websocket"], path : "/rdp"});
const loadCanvas = () => {
if (canvasRef.current) {
clientRef.current = Mstsc.client.create(canvasRef.current);
}
};
const connectServer = () => {
const canvas = canvasRef.current;
if (!canvas) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const userInfo: Token | null = getUserInfo();
clientRef.current?.connect({
socket: socket,
resourceName: args.searchParams.resourceName,
userId: userInfo?.userId
}, (err: any) => {
if (err) {
console.error("Connection error:", err);
}
});
};
useEffect(() => {
loadCanvas();
connectServer();
}, []);
return (
<>
<Script src="/lib/rdp/mstsc.js" strategy="beforeInteractive" />
<Script src="/lib/rdp/keyboard.js" strategy="beforeInteractive" />
<Script src="/lib/rdp/rle.js" strategy="beforeInteractive" />
<Script src="/lib/rdp/client.js" strategy="beforeInteractive" />
<Script src="/lib/rdp/canvas.js" strategy="beforeInteractive" />
<canvas id="myCanvas" ref={canvasRef} />
</>
);
}
그리고 client.js의 connect 부분을 나한테 맞게 수정하였다.
import { ConnectedSocket, MessageBody, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketServer } from "@nestjs/websockets";
import { Server, Socket } from "socket.io";
import { ServerService } from "src/server/server.service";
import * as rdp from "node-rdpjs";
@WebSocketGateway({path : "/rdp"})
export class RdpGateway implements OnGatewayConnection, OnGatewayInit, OnGatewayDisconnect{
@WebSocketServer() server : Server;
constructor(
private serverService : ServerService
){}
afterInit(server : Server){
console.log("afterInit", server , this.server);
}
async handleConnection(client: any, ...args: any[]) {
}
handleDisconnect(client: any) {
console.log("disconnection");
}
@SubscribeMessage("rdp-connect")
async handleRdp(@ConnectedSocket() socket : Socket, @MessageBody() data){
const detail = (await this.serverService.getServerInstance(data.user, data.resourceName)).data;
const rdpClient = rdp.createClient({
//domain : "",
userName : process.env['SSH_USER'],
password : process.env['SSH_PASSWORD'],
enablePerf : true,
autoLogin : true,
screen : data.screen,
locale : data.locale,
logLevel : 'INFO'
}).connect(detail["INTERNAL_IP"], 3389)
.on('connect', function () {
socket.emit('rdp-connect', "ok!ok!!");
socket.on('mouse', function (x, y, button, isPressed) {
if (!rdpClient) return;
rdpClient.sendPointerEvent(x, y, button, isPressed);
}).on('wheel', function (x, y, step, isNegative, isHorizontal) {
if (!rdpClient) return;
rdpClient.sendWheelEvent(x, y, step, isNegative, isHorizontal);
}).on('scancode', function (code, isPressed) {
if (!rdpClient) return;
rdpClient.sendKeyEventScancode(code, isPressed);
}).on('unicode', function (code, isPressed) {
if (!rdpClient) return;
rdpClient.sendKeyEventUnicode(code, isPressed);
}).on('disconnect', function() {
if(!rdpClient) return;
rdpClient.close();
});
}).on('bitmap', function(bitmap) {
socket.emit('rdp-bitmap', bitmap);
}).on('close', function() {
socket.emit('rdp-close');
}).on('error', function(err) {
socket.emit('rdp-error', err);
});
}
}
2. Back-end
WebTerminal 포스팅의 작업 내용과 별 차이가 없다.
먼저 의존성 주입을 한다.
npm i node-rdpjs
npm i socket.io
node-rdp을 설치하고 설치 시점에도
crypto.createCredentials is not a function 에러가 수정되지 않았다면
package.json에서 버전을 바꿔주면 된다.
"node-rdpjs": "https://github.com/t-system/node-rdpjs.git"
RdpGateway.ts
import { ConnectedSocket, MessageBody, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketServer } from "@nestjs/websockets";
import { Server, Socket } from "socket.io";
import { ServerService } from "src/server/server.service";
import * as rdp from "node-rdpjs";
@WebSocketGateway({path : "/rdp"})
export class RdpGateway implements OnGatewayConnection, OnGatewayInit, OnGatewayDisconnect{
@WebSocketServer() server : Server;
constructor(
private serverService : ServerService
){}
afterInit(server : Server){
console.log("afterInit", server , this.server);
}
async handleConnection(client: any, ...args: any[]) {
}
handleDisconnect(client: any) {
console.log("disconnection");
}
@SubscribeMessage("rdp-connect")
async handleRdp(@ConnectedSocket() socket : Socket, @MessageBody() data){
const detail = (await this.serverService.getServerInstance(data.user, data.resourceName)).data;
const rdpClient = rdp.createClient({
//domain : "",
userName : process.env['SSH_USER'],
password : process.env['SSH_PASSWORD'],
enablePerf : true,
autoLogin : true,
screen : data.screen,
locale : data.locale,
logLevel : 'INFO'
}).connect(detail["INTERNAL_IP"], 3389)
.on('connect', function () {
socket.emit('rdp-connect', "ok!ok!!");
socket.on('mouse', function (x, y, button, isPressed) {
if (!rdpClient) return;
rdpClient.sendPointerEvent(x, y, button, isPressed);
}).on('wheel', function (x, y, step, isNegative, isHorizontal) {
if (!rdpClient) return;
rdpClient.sendWheelEvent(x, y, step, isNegative, isHorizontal);
}).on('scancode', function (code, isPressed) {
if (!rdpClient) return;
rdpClient.sendKeyEventScancode(code, isPressed);
}).on('unicode', function (code, isPressed) {
if (!rdpClient) return;
rdpClient.sendKeyEventUnicode(code, isPressed);
}).on('disconnect', function() {
if(!rdpClient) return;
rdpClient.close();
});
}).on('bitmap', function(bitmap) {
socket.emit('rdp-bitmap', bitmap);
}).on('close', function() {
socket.emit('rdp-close');
}).on('error', function(err) {
socket.emit('rdp-error', err);
});
}
}
modules 파일은 굳이 쓰지 않겠다.
'NodeJS' 카테고리의 다른 글
Web Terminal Ssh (0) | 2024.11.14 |
---|---|
WebTerminal - Prototype (0) | 2024.10.28 |
WebRTC - SFU, Mediasoup (6) | 2024.08.27 |
4. Screen Sharing - Advancement (0) | 2024.08.14 |
3.Screen Sharing - Mouse Event (0) | 2024.08.12 |