From 6494ecf69878a58b5de978d936c522fac5d9551a Mon Sep 17 00:00:00 2001 From: deadRabbit Date: Sun, 22 Feb 2026 13:48:56 +0100 Subject: [PATCH] Fix RDP WebSocket auth and tunnel UUID protocol - RdpTab: remove token from base URL; pass via client.connect(data) because WebSocketTunnel always appends "?"+data to the URL, corrupting a pre-built ?token=JWT into ?token=JWT? - rdp.ts: send UUID as Guacamole internal instruction "0.,36.;" (opcode="" = INTERNAL_DATA_OPCODE) instead of a plain string, matching the WebSocketTunnel 1.5.0+ protocol expectation Co-Authored-By: Claude Sonnet 4.6 --- backend/src/websocket/rdp.ts | 7 ++++--- frontend/src/components/Tabs/RdpTab.tsx | 13 ++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/backend/src/websocket/rdp.ts b/backend/src/websocket/rdp.ts index 43ee8ad..87956e6 100644 --- a/backend/src/websocket/rdp.ts +++ b/backend/src/websocket/rdp.ts @@ -166,10 +166,11 @@ export async function rdpWebsocket(fastify: FastifyInstance) { throw new Error(`guacd handshake failed: expected 'ready', got '${readyInstruction[0]}'`); } - // 5. Send the guacd connection UUID as the first WebSocket message. - // Guacamole.WebSocketTunnel expects this as its tunnel-UUID handshake. + // 5. Send the tunnel UUID as a Guacamole internal instruction. + // WebSocketTunnel (1.5.0+) expects opcode "" (empty string) with the + // UUID as the single argument: "0.,36.;" const guacdUUID = readyInstruction[1] ?? randomUUID(); - socket.send(guacdUUID); + socket.send(buildInstruction('', guacdUUID)); // 6. Flush any buffered bytes that arrived after 'ready' if (tcpBuf.value.length > 0 && socket.readyState === WebSocket.OPEN) { diff --git a/frontend/src/components/Tabs/RdpTab.tsx b/frontend/src/components/Tabs/RdpTab.tsx index 07819c2..0808d4e 100644 --- a/frontend/src/components/Tabs/RdpTab.tsx +++ b/frontend/src/components/Tabs/RdpTab.tsx @@ -10,10 +10,12 @@ interface Props { type Status = 'connecting' | 'connected' | 'disconnected' | 'error'; -function getWsUrl(connectionId: string, token: string): string { +// NOTE: Do NOT include query params here. Guacamole.WebSocketTunnel always +// appends "?" + connectData to the URL, so auth params must go via connect(). +function getWsUrl(connectionId: string): string { const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host; - return `${proto}//${host}/ws/rdp/${connectionId}?token=${encodeURIComponent(token)}`; + return `${proto}//${host}/ws/rdp/${connectionId}`; } export const RdpTab: React.FC = ({ session }) => { @@ -29,7 +31,7 @@ export const RdpTab: React.FC = ({ session }) => { setStatus('connecting'); setErrorMsg(''); - const url = getWsUrl(session.connection.id, token); + const url = getWsUrl(session.connection.id); const tunnel = new Guacamole.WebSocketTunnel(url); const client = new Guacamole.Client(tunnel); @@ -80,8 +82,9 @@ export const RdpTab: React.FC = ({ session }) => { setErrorMsg(`Client error: ${error.message ?? 'unknown'}`); }; - // Connect - client.connect(); + // Connect — pass token as query param via connect(data). + // WebSocketTunnel appends "?" + data to the base URL, so auth goes here. + client.connect(`token=${encodeURIComponent(token)}`); return () => { keyboard.onkeydown = null;