fix(rdp): override Guacamole.Tunnel instance methods, fix sendMessage encoding
This commit is contained in:
parent
e3a978b639
commit
76db0a6936
@ -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: '<raw guac instruction>' }
|
||||
* Gateway → Browser: { type: 'connected', hostId, hostName }
|
||||
* Gateway → Browser: { type: 'guac', instruction: '<raw 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: "<len>.<value>,<len>.<value>,...;"
|
||||
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: "<len>.<opcode>,<len>.<arg1>,...;"
|
||||
* 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() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user