From f124d4b7d2c88c5f542546023edfd2c29d1d1660 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sat, 14 Mar 2026 02:40:37 -0400 Subject: [PATCH] fix(rdp): convert to manual ws.Server, fix URL path, fix double session --- backend/src/main.ts | 19 ++++++++- backend/src/rdp/rdp.gateway.ts | 40 ++++++++----------- frontend/components/rdp/RdpCanvas.vue | 2 + .../components/session/SessionContainer.vue | 1 + frontend/composables/useRdp.ts | 18 ++++++--- 5 files changed, 51 insertions(+), 29 deletions(-) diff --git a/backend/src/main.ts b/backend/src/main.ts index b9cc4a2..0144a73 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -5,6 +5,7 @@ import { AppModule } from './app.module'; import { WebSocketServer } from 'ws'; import { TerminalGateway } from './terminal/terminal.gateway'; import { SftpGateway } from './terminal/sftp.gateway'; +import { RdpGateway } from './rdp/rdp.gateway'; // Crash handlers — catch whatever is killing the process process.on('uncaughtException', (err) => { @@ -30,9 +31,11 @@ async function bootstrap() { const server = app.getHttpServer(); const terminalGateway = app.get(TerminalGateway); const sftpGateway = app.get(SftpGateway); + const rdpGateway = app.get(RdpGateway); const terminalWss = new WebSocketServer({ noServer: true }); const sftpWss = new WebSocketServer({ noServer: true }); + const rdpWss = new WebSocketServer({ noServer: true }); terminalWss.on('connection', (ws, req) => { try { @@ -52,6 +55,15 @@ async function bootstrap() { } }); + rdpWss.on('connection', (ws, req) => { + try { + console.log(`[WS] RDP connection established`); + rdpGateway.handleConnection(ws, req); + } catch (err: any) { + console.error(`[FATAL] RDP handleConnection crashed: ${err.message}\n${err.stack}`); + } + }); + // Remove ALL existing upgrade listeners (WsAdapter's) so we handle upgrades first const existingListeners = server.listeners('upgrade'); server.removeAllListeners('upgrade'); @@ -72,8 +84,13 @@ async function bootstrap() { console.log(`[WS] SFTP upgrade complete`); sftpWss.emit('connection', ws, req); }); + } else if (pathname === '/api/ws/rdp') { + rdpWss.handleUpgrade(req, socket, head, (ws) => { + console.log(`[WS] RDP upgrade complete`); + rdpWss.emit('connection', ws, req); + }); } else { - // Pass to WsAdapter's original handlers (for RDP etc) + // Pass to WsAdapter's original handlers for (const listener of existingListeners) { listener.call(server, req, socket, head); } diff --git a/backend/src/rdp/rdp.gateway.ts b/backend/src/rdp/rdp.gateway.ts index e0ad357..e5a1754 100644 --- a/backend/src/rdp/rdp.gateway.ts +++ b/backend/src/rdp/rdp.gateway.ts @@ -1,11 +1,4 @@ -import { - WebSocketGateway, - WebSocketServer, - OnGatewayConnection, - OnGatewayDisconnect, -} from '@nestjs/websockets'; -import { Logger } from '@nestjs/common'; -import { Server } from 'ws'; +import { Injectable, Logger } from '@nestjs/common'; import * as net from 'net'; import { WsAuthGuard } from '../auth/ws-auth.guard'; import { GuacamoleService } from './guacamole.service'; @@ -13,9 +6,8 @@ import { CredentialsService } from '../vault/credentials.service'; import { HostsService } from '../connections/hosts.service'; import { PrismaService } from '../prisma/prisma.service'; -@WebSocketGateway({ path: '/ws/rdp' }) -export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect { - @WebSocketServer() server: Server; +@Injectable() +export class RdpGateway { private readonly logger = new Logger(RdpGateway.name); // Maps browser WebSocket client → live guacd TCP socket @@ -29,8 +21,8 @@ export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect { private wsAuth: WsAuthGuard, ) {} - handleConnection(client: any) { - const user = this.wsAuth.validateClient(client); + handleConnection(client: any, req: any) { + const user = this.wsAuth.validateClient(client, req); if (!user) { client.close(4001, 'Unauthorized'); return; @@ -41,6 +33,7 @@ export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect { client.on('message', async (raw: Buffer) => { try { const msg = JSON.parse(raw.toString()); + this.logger.log(`[RDP] Message: ${msg.type}`); if (msg.type === 'connect') { await this.handleConnect(client, msg); @@ -56,15 +49,16 @@ export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect { this.send(client, { type: 'error', message: err.message }); } }); - } - handleDisconnect(client: any) { - const socket = this.clientSockets.get(client); - if (socket) { - socket.destroy(); - this.clientSockets.delete(client); - this.logger.log('RDP WS disconnected — guacd socket destroyed'); - } + client.on('close', () => { + this.logger.log('RDP WS disconnected'); + const socket = this.clientSockets.get(client); + if (socket) { + socket.destroy(); + this.clientSockets.delete(client); + this.logger.log('guacd socket destroyed on WS close'); + } + }); } private async handleConnect(client: any, msg: any) { @@ -90,7 +84,7 @@ export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect { dpi: msg.dpi || 96, security: msg.security || 'any', colorDepth: msg.colorDepth || 24, - ignoreCert: true, // default permissive — expose as setting in Task 15+ + ignoreCert: true, }); this.clientSockets.set(client, socket); @@ -115,7 +109,7 @@ export class RdpGateway implements OnGatewayConnection, OnGatewayDisconnect { this.send(client, { type: 'error', message: err.message }); }); - // Connection tracking — same pattern as SSH gateway + // Connection tracking this.hosts.touchLastConnected(host.id).catch(() => {}); this.prisma.connectionLog .create({ data: { hostId: host.id, protocol: 'rdp' } }) diff --git a/frontend/components/rdp/RdpCanvas.vue b/frontend/components/rdp/RdpCanvas.vue index 0aa913b..58e0f9c 100644 --- a/frontend/components/rdp/RdpCanvas.vue +++ b/frontend/components/rdp/RdpCanvas.vue @@ -5,6 +5,7 @@ import { useRdp } from '~/composables/useRdp' const props = defineProps<{ hostId: number hostName: string + sessionId: string color?: string | null }>() @@ -27,6 +28,7 @@ onMounted(() => { props.hostId, props.hostName, props.color ?? null, + props.sessionId, { width: container.value.clientWidth, height: container.value.clientHeight, diff --git a/frontend/components/session/SessionContainer.vue b/frontend/components/session/SessionContainer.vue index 36496ce..cbc9eca 100644 --- a/frontend/components/session/SessionContainer.vue +++ b/frontend/components/session/SessionContainer.vue @@ -70,6 +70,7 @@ function handleRdpClipboard(sessionId: string, text: string) { :ref="(el) => setRdpRef(session.id, el)" :host-id="session.hostId" :host-name="session.hostName" + :session-id="session.id" :color="session.color" /> { + console.log('[RDP] WebSocket open, sending connect handshake') // Step 1: send our JSON connect handshake this.ws!.send(JSON.stringify(this.connectMsg)) } @@ -159,14 +161,17 @@ export function useRdp() { hostId: number, hostName: string, color: string | null, + pendingSessionId: string, options?: { width?: number; height?: number }, ) { const proto = location.protocol === 'https:' ? 'wss' : 'ws' - const wsUrl = `${proto}://${location.host}/ws/rdp?token=${auth.token}` + const wsUrl = `${proto}://${location.host}/api/ws/rdp?token=${auth.token}` const width = options?.width || container.clientWidth || 1920 const height = options?.height || container.clientHeight || 1080 + console.log(`[RDP] Connecting to ${wsUrl} for hostId=${hostId} (${width}x${height})`) + const tunnel = new JsonWsTunnel(wsUrl, { type: 'connect', hostId, @@ -176,12 +181,14 @@ export function useRdp() { const client = new Guacamole.Client(tunnel) - // Session store ID — created optimistically, will be confirmed on 'connected' - const sessionId = `rdp-${hostId}-${Date.now()}` + // Session ID is passed in by the caller (the pending session created in connectHost) + const sessionId = pendingSessionId // Wire tunnel callbacks tunnel.onConnected = (_resolvedHostId: number, resolvedHostName: string) => { - sessions.addSession({ + console.log(`[RDP] Connected to ${resolvedHostName}`) + // Update the pending session with the resolved host name + sessions.replaceSession(sessionId, { key: sessionId, id: sessionId, hostId, @@ -192,7 +199,8 @@ export function useRdp() { }) } - tunnel.onDisconnected = (_reason: string) => { + tunnel.onDisconnected = (reason: string) => { + console.log(`[RDP] Disconnected: ${reason}`) sessions.removeSession(sessionId) client.disconnect() }