From 9d3a93bea917d68673b64b53536a2ab0f60e052d Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sat, 14 Mar 2026 05:05:00 -0400 Subject: [PATCH] fix(rdp): parse guacd args response and send matching positional connect values --- backend/src/rdp/guacamole.service.ts | 122 +++++++++++++++++++-------- 1 file changed, 87 insertions(+), 35 deletions(-) diff --git a/backend/src/rdp/guacamole.service.ts b/backend/src/rdp/guacamole.service.ts index fafc9d7..da6d7d7 100644 --- a/backend/src/rdp/guacamole.service.ts +++ b/backend/src/rdp/guacamole.service.ts @@ -54,9 +54,19 @@ export class GuacamoleService { // Clear the connect timeout — handshake completed socket.setTimeout(0); - // Phase 2: CONNECT with RDP parameters - const connectInstruction = this.buildConnectInstruction(params); - this.logger.debug(`Sending connect instruction to guacd`); + // Parse the args instruction to get expected parameter names + const argsInstruction = buffer.substring(0, semicolonIdx + 1); + const argNames = this.decode(argsInstruction); + + // First element is the opcode ("args"), rest are parameter names + if (argNames[0] === 'args') { + argNames.shift(); + } + this.logger.log(`guacd expects ${argNames.length} args: ${argNames.join(', ')}`); + + // Phase 2: CONNECT — send values in the exact order guacd expects + const connectInstruction = this.buildConnectInstruction(params, argNames); + this.logger.debug(`Sending connect instruction with ${argNames.length} values`); socket.write(connectInstruction); resolve(socket); @@ -78,31 +88,33 @@ export class GuacamoleService { }); } - private buildConnectInstruction(params: { - hostname: string; - port: number; - username: string; - password: string; - domain?: string; - width: number; - height: number; - dpi?: number; - security?: string; - colorDepth?: number; - ignoreCert?: boolean; - }): string { - // The connect instruction sends all RDP connection parameters as positional args. - // guacd expects exactly the args it listed in the "args" response to SELECT. - // We send the full standard RDP parameter set. - const args: Record = { - hostname: params.hostname, - port: String(params.port), - username: params.username, - password: params.password, - width: String(params.width), - height: String(params.height), - dpi: String(params.dpi || 96), - security: params.security || 'any', + private buildConnectInstruction( + params: { + hostname: string; + port: number; + username: string; + password: string; + domain?: string; + width: number; + height: number; + dpi?: number; + security?: string; + colorDepth?: number; + ignoreCert?: boolean; + }, + argNames: string[], + ): string { + // Map our params to guacd's expected arg names + const paramMap: Record = { + 'hostname': params.hostname, + 'port': String(params.port), + 'username': params.username, + 'password': params.password, + 'domain': params.domain || '', + 'width': String(params.width), + 'height': String(params.height), + 'dpi': String(params.dpi || 96), + 'security': params.security || 'any', 'color-depth': String(params.colorDepth || 24), 'ignore-cert': params.ignoreCert !== false ? 'true' : 'false', 'disable-audio': 'false', @@ -110,14 +122,55 @@ export class GuacamoleService { 'enable-theming': 'true', 'enable-font-smoothing': 'true', 'resize-method': 'reconnect', + 'server-layout': '', + 'timezone': '', + 'console': '', + 'initial-program': '', + 'client-name': 'Wraith', + 'enable-full-window-drag': 'false', + 'enable-desktop-composition': 'false', + 'enable-menu-animations': 'false', + 'disable-bitmap-caching': 'false', + 'disable-offscreen-caching': 'false', + 'disable-glyph-caching': 'false', + 'preconnection-id': '', + 'preconnection-blob': '', + 'enable-sftp': 'false', + 'sftp-hostname': '', + 'sftp-port': '', + 'sftp-username': '', + 'sftp-password': '', + 'sftp-private-key': '', + 'sftp-passphrase': '', + 'sftp-directory': '', + 'sftp-root-directory': '', + 'sftp-server-alive-interval': '', + 'recording-path': '', + 'recording-name': '', + 'recording-exclude-output': '', + 'recording-exclude-mouse': '', + 'recording-include-keys': '', + 'create-recording-path': '', + 'remote-app': '', + 'remote-app-dir': '', + 'remote-app-args': '', + 'gateway-hostname': '', + 'gateway-port': '', + 'gateway-domain': '', + 'gateway-username': '', + 'gateway-password': '', + 'load-balance-info': '', + 'normalize-clipboard': '', + 'force-lossless': '', + 'wol-send-packet': '', + 'wol-mac-addr': '', + 'wol-broadcast-addr': '', + 'wol-udp-port': '', + 'wol-wait-time': '', }; - if (params.domain) { - args['domain'] = params.domain; - } - - // Build the connect instruction with opcode + all arg values - const values = Object.values(args); + // Build values array matching the exact order guacd expects + const values = argNames.map((name) => paramMap[name] ?? ''); return this.encode('connect', ...values); } @@ -133,7 +186,6 @@ export class GuacamoleService { /** * Decode a Guacamole instruction string back to a string array. - * Handles multiple instructions in a single buffer chunk. */ decode(instruction: string): string[] { const parts: string[] = [];