WebRDP

배고픈 징징이 ㅣ 2024. 12. 18. 11:17

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