import { Injectable, Logger } from '@nestjs/common'; import * as net from 'net'; import { WsAuthGuard } from '../auth/ws-auth.guard'; import { GuacamoleService } from './guacamole.service'; import { CredentialsService } from '../vault/credentials.service'; import { HostsService } from '../connections/hosts.service'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class RdpGateway { private readonly logger = new Logger(RdpGateway.name); // Maps browser WebSocket client → live guacd TCP socket private clientSockets = new Map(); constructor( private guacamole: GuacamoleService, private credentials: CredentialsService, private hosts: HostsService, private prisma: PrismaService, private wsAuth: WsAuthGuard, ) {} handleConnection(client: any, req: any) { const user = this.wsAuth.validateClient(client, req); if (!user) { client.close(4001, 'Unauthorized'); return; } this.logger.log(`RDP WS connected: ${user.email}`); 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); } else if (msg.type === 'guac') { // Forward raw Guacamole instruction from browser to guacd TCP socket const socket = this.clientSockets.get(client); if (socket && !socket.destroyed) { socket.write(msg.instruction); } } } catch (err: any) { this.logger.error(`RDP message error: ${err.message}`); this.send(client, { type: 'error', message: err.message }); } }); 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) { const host = await this.hosts.findOne(msg.hostId); // Decrypt credentials if attached to host const cred = host.credentialId ? await this.credentials.decryptForConnection(host.credentialId) : null; this.logger.log( `Opening RDP tunnel: ${host.hostname}:${host.port} for host "${host.name}"`, ); const socket = await this.guacamole.connect({ hostname: host.hostname, port: host.port, username: cred?.username || '', password: cred?.password || '', domain: cred?.domain || undefined, width: msg.width || 1920, height: msg.height || 1080, dpi: msg.dpi || 96, security: msg.security || 'any', colorDepth: msg.colorDepth || 24, ignoreCert: true, }); this.clientSockets.set(client, socket); // Pipe guacd → browser: wrap raw Guacamole instruction bytes in JSON envelope socket.on('data', (data: Buffer) => { if (client.readyState === 1 /* WebSocket.OPEN */) { client.send( JSON.stringify({ type: 'guac', instruction: data.toString('utf-8') }), ); } }); socket.on('close', () => { this.logger.log(`guacd socket closed for host ${host.id}`); this.send(client, { type: 'disconnected', reason: 'RDP session closed' }); this.clientSockets.delete(client); }); socket.on('error', (err) => { this.logger.error(`guacd socket error for host ${host.id}: ${err.message}`); this.send(client, { type: 'error', message: err.message }); }); // Connection tracking this.hosts.touchLastConnected(host.id).catch(() => {}); this.prisma.connectionLog .create({ data: { hostId: host.id, protocol: 'rdp' } }) .catch(() => {}); this.send(client, { type: 'connected', hostId: host.id, hostName: host.name }); } private send(client: any, data: any) { if (client.readyState === 1 /* WebSocket.OPEN */) { client.send(JSON.stringify(data)); } } }