From 76db0a69360e463ffc69745c441281ac284a781f Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sat, 14 Mar 2026 04:39:44 -0400 Subject: [PATCH] fix(rdp): override Guacamole.Tunnel instance methods, fix sendMessage encoding --- frontend/composables/useRdp.ts | 57 +++++++++++++--------------------- 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/frontend/composables/useRdp.ts b/frontend/composables/useRdp.ts index ce555ad..58a687c 100644 --- a/frontend/composables/useRdp.ts +++ b/frontend/composables/useRdp.ts @@ -13,23 +13,15 @@ import { useSessionStore } from '~/stores/session.store' * - This tunnel bridges the two: guacamole-common-js sees raw Guacamole * instructions while our gateway sees consistent JSON messages. * - * Protocol: - * Browser → Gateway: { type: 'connect', hostId, width, height, ... } - * Browser → Gateway: { type: 'guac', instruction: '' } - * Gateway → Browser: { type: 'connected', hostId, hostName } - * Gateway → Browser: { type: 'guac', instruction: '' } - * Gateway → Browser: { type: 'error', message: '...' } - * Gateway → Browser: { type: 'disconnected', reason: '...' } + * IMPORTANT: Guacamole.Tunnel assigns connect/sendMessage/disconnect as + * instance properties in its constructor (not prototype methods). We MUST + * reassign them in our constructor AFTER super() to override the no-ops. */ class JsonWsTunnel extends Guacamole.Tunnel { private ws: WebSocket | null = null private readonly wsUrl: string private readonly connectMsg: object - // Callbacks set by Guacamole.Client after we're constructed - oninstruction: ((opcode: string, args: string[]) => void) | null = null - onerror: ((status: Guacamole.Status) => void) | null = null - // Expose for external callers (connected event, disconnect action) onConnected: ((hostId: number, hostName: string) => void) | null = null onDisconnected: ((reason: string) => void) | null = null @@ -39,15 +31,19 @@ class JsonWsTunnel extends Guacamole.Tunnel { super() this.wsUrl = wsUrl this.connectMsg = connectMsg + + // Override instance methods set by Guacamole.Tunnel constructor + this.connect = this._connect.bind(this) + this.disconnect = this._disconnect.bind(this) + this.sendMessage = this._sendMessage.bind(this) } - connect(_data?: string) { + private _connect(_data?: string) { console.log('[RDP] Tunnel opening WebSocket:', this.wsUrl) this.ws = new WebSocket(this.wsUrl) this.ws.onopen = () => { console.log('[RDP] WebSocket open, sending connect handshake') - // Step 1: send our JSON connect handshake this.ws!.send(JSON.stringify(this.connectMsg)) } @@ -56,15 +52,10 @@ class JsonWsTunnel extends Guacamole.Tunnel { switch (msg.type) { case 'connected': - // Gateway completed the guacd SELECT→CONNECT handshake. - // Notify RdpCanvas so it can finalize the session store entry. this.onConnected?.(msg.hostId, msg.hostName) break case 'guac': { - // Raw Guacamole instruction from guacd — parse and dispatch to client. - // Instructions arrive as: "4.size,4.1024,3.768;" or multiple batched. - // We dispatch each complete instruction individually. const instruction: string = msg.instruction this._dispatchInstructions(instruction) break @@ -93,27 +84,28 @@ class JsonWsTunnel extends Guacamole.Tunnel { } } - disconnect() { + private _disconnect() { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.close() } this.ws = null } - sendMessage(message: string) { - // Guacamole.Client calls this with raw Guacamole instruction strings. - // Wrap in JSON envelope and forward to gateway → guacd. + private _sendMessage(...elements: any[]) { + // Guacamole.Client calls sendMessage(opcode, arg1, arg2, ...) with individual elements. + // Encode into Guacamole wire format: ".,.,...;" if (this.ws?.readyState === WebSocket.OPEN) { - this.ws.send(JSON.stringify({ type: 'guac', instruction: message })) + const instruction = elements.map((e: any) => { + const s = String(e) + return `${s.length}.${s}` + }).join(',') + ';' + this.ws.send(JSON.stringify({ type: 'guac', instruction })) } } /** * Parse one or more Guacamole instructions from a raw buffer string * and call oninstruction for each. - * - * Guacamole instruction format: ".,.,...;" - * Multiple instructions may arrive in one data chunk. */ private _dispatchInstructions(raw: string) { if (!this.oninstruction) return @@ -121,14 +113,13 @@ class JsonWsTunnel extends Guacamole.Tunnel { let remaining = raw while (remaining.length > 0) { const semicolonIdx = remaining.indexOf(';') - if (semicolonIdx === -1) break // incomplete instruction — guacd shouldn't send partials but be safe + if (semicolonIdx === -1) break const instruction = remaining.substring(0, semicolonIdx) remaining = remaining.substring(semicolonIdx + 1) if (!instruction) continue - // Parse length-prefixed fields const parts: string[] = [] let pos = 0 while (pos < instruction.length) { @@ -138,7 +129,7 @@ class JsonWsTunnel extends Guacamole.Tunnel { if (isNaN(len)) break const value = instruction.substring(dotIdx + 1, dotIdx + 1 + len) parts.push(value) - pos = dotIdx + 1 + len + 1 // skip comma separator + pos = dotIdx + 1 + len + 1 } if (parts.length > 0) { @@ -181,13 +172,10 @@ export function useRdp() { const client = new Guacamole.Client(tunnel) - // 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) => { console.log(`[RDP] Connected to ${resolvedHostName}`) - // Update the pending session with the resolved host name sessions.replaceSession(sessionId, { key: sessionId, id: sessionId, @@ -222,13 +210,12 @@ export function useRdp() { }, ) - // Keyboard input — attach to document so key events fire even when - // focus is on other elements (toolbar buttons, etc.) + // Keyboard input const keyboard = new Guacamole.Keyboard(document) keyboard.onkeydown = (keysym: number) => client.sendKeyEvent(1, keysym) keyboard.onkeyup = (keysym: number) => client.sendKeyEvent(0, keysym) - // Initiate the WebSocket connection (triggers connect handshake) + // Initiate the WebSocket connection client.connect() function disconnect() {