diff --git a/backend/src/rdp/guacamole.service.ts b/backend/src/rdp/guacamole.service.ts index de60cc8..5013997 100644 --- a/backend/src/rdp/guacamole.service.ts +++ b/backend/src/rdp/guacamole.service.ts @@ -66,7 +66,10 @@ export class GuacamoleService { // 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`); + this.logger.log( + `Sending CONNECT: host=${params.hostname}:${params.port} user=${params.username} domain=${params.domain || '(none)'} ` + + `security=${params.security || 'any'} size=${params.width}x${params.height} ignoreCert=${params.ignoreCert !== false}`, + ); socket.write(connectInstruction); resolve(socket); diff --git a/backend/src/rdp/rdp.gateway.ts b/backend/src/rdp/rdp.gateway.ts index b7fd229..4c3d11e 100644 --- a/backend/src/rdp/rdp.gateway.ts +++ b/backend/src/rdp/rdp.gateway.ts @@ -89,17 +89,31 @@ export class RdpGateway { this.clientSockets.set(client, socket); - // Pipe guacd → browser: wrap raw Guacamole instruction bytes in JSON envelope + // Pipe guacd → browser: buffer TCP stream into complete Guacamole instructions. + // TCP is a stream protocol — data events can contain partial instructions, + // multiple instructions, or any combination. We must only forward complete + // instructions (terminated by ';') to the browser. let guacMsgCount = 0; + let tcpBuffer = ''; socket.on('data', (data: Buffer) => { - const instruction = data.toString('utf-8'); - guacMsgCount++; - // Log first 5 instructions and any errors - if (guacMsgCount <= 5 || instruction.includes('error')) { - this.logger.log(`[guacd→browser #${guacMsgCount}] ${instruction.substring(0, 300)}`); - } - if (client.readyState === 1 /* WebSocket.OPEN */) { - client.send(JSON.stringify({ type: 'guac', instruction })); + tcpBuffer += data.toString('utf-8'); + + // Extract all complete instructions from the buffer + let semicolonIdx: number; + while ((semicolonIdx = tcpBuffer.indexOf(';')) !== -1) { + const instruction = tcpBuffer.substring(0, semicolonIdx + 1); + tcpBuffer = tcpBuffer.substring(semicolonIdx + 1); + + guacMsgCount++; + // Log first 10 instructions and any errors for diagnostics + if (guacMsgCount <= 10 || instruction.includes('error')) { + this.logger.log( + `[guacd→browser #${guacMsgCount}] ${instruction.substring(0, 300)}`, + ); + } + if (client.readyState === 1 /* WebSocket.OPEN */) { + client.send(JSON.stringify({ type: 'guac', instruction })); + } } }); diff --git a/frontend/composables/useRdp.ts b/frontend/composables/useRdp.ts index 91a7aad..e697656 100644 --- a/frontend/composables/useRdp.ts +++ b/frontend/composables/useRdp.ts @@ -168,6 +168,28 @@ export function useRdp() { console.error('[RDP] Gateway error:', message) } + // Handle Guacamole-level errors (NLA failure, auth failure, etc.) + client.onerror = (status: Guacamole.Status) => { + const code = status.code + const msg = status.message || 'Unknown error' + console.error(`[RDP] Guacamole error: code=${code} message=${msg}`) + // Surface error via gateway error callback so UI can display it + wrapper.onGatewayError?.(`RDP connection failed: ${msg}`) + } + + // Track connection state transitions for diagnostics + client.onstatechange = (state: number) => { + const states: Record = { + 0: 'IDLE', + 1: 'CONNECTING', + 2: 'WAITING', + 3: 'CONNECTED', + 4: 'DISCONNECTING', + 5: 'DISCONNECTED', + } + console.log(`[RDP] State: ${states[state] || state}`) + } + // Attach Guacamole display element to container const displayEl = client.getDisplay().getElement() container.appendChild(displayEl)