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
|
* - This tunnel bridges the two: guacamole-common-js sees raw Guacamole
|
||||||
* instructions while our gateway sees consistent JSON messages.
|
* instructions while our gateway sees consistent JSON messages.
|
||||||
*
|
*
|
||||||
* Protocol:
|
* IMPORTANT: Guacamole.Tunnel assigns connect/sendMessage/disconnect as
|
||||||
* Browser → Gateway: { type: 'connect', hostId, width, height, ... }
|
* instance properties in its constructor (not prototype methods). We MUST
|
||||||
* Browser → Gateway: { type: 'guac', instruction: '<raw guac instruction>' }
|
* reassign them in our constructor AFTER super() to override the no-ops.
|
||||||
* Gateway → Browser: { type: 'connected', hostId, hostName }
|
|
||||||
* Gateway → Browser: { type: 'guac', instruction: '<raw guac instruction>' }
|
|
||||||
* Gateway → Browser: { type: 'error', message: '...' }
|
|
||||||
* Gateway → Browser: { type: 'disconnected', reason: '...' }
|
|
||||||
*/
|
*/
|
||||||
class JsonWsTunnel extends Guacamole.Tunnel {
|
class JsonWsTunnel extends Guacamole.Tunnel {
|
||||||
private ws: WebSocket | null = null
|
private ws: WebSocket | null = null
|
||||||
private readonly wsUrl: string
|
private readonly wsUrl: string
|
||||||
private readonly connectMsg: object
|
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)
|
// Expose for external callers (connected event, disconnect action)
|
||||||
onConnected: ((hostId: number, hostName: string) => void) | null = null
|
onConnected: ((hostId: number, hostName: string) => void) | null = null
|
||||||
onDisconnected: ((reason: string) => void) | null = null
|
onDisconnected: ((reason: string) => void) | null = null
|
||||||
@ -39,15 +31,19 @@ class JsonWsTunnel extends Guacamole.Tunnel {
|
|||||||
super()
|
super()
|
||||||
this.wsUrl = wsUrl
|
this.wsUrl = wsUrl
|
||||||
this.connectMsg = connectMsg
|
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)
|
console.log('[RDP] Tunnel opening WebSocket:', this.wsUrl)
|
||||||
this.ws = new WebSocket(this.wsUrl)
|
this.ws = new WebSocket(this.wsUrl)
|
||||||
|
|
||||||
this.ws.onopen = () => {
|
this.ws.onopen = () => {
|
||||||
console.log('[RDP] WebSocket open, sending connect handshake')
|
console.log('[RDP] WebSocket open, sending connect handshake')
|
||||||
// Step 1: send our JSON connect handshake
|
|
||||||
this.ws!.send(JSON.stringify(this.connectMsg))
|
this.ws!.send(JSON.stringify(this.connectMsg))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,15 +52,10 @@ class JsonWsTunnel extends Guacamole.Tunnel {
|
|||||||
|
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'connected':
|
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)
|
this.onConnected?.(msg.hostId, msg.hostName)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'guac': {
|
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
|
const instruction: string = msg.instruction
|
||||||
this._dispatchInstructions(instruction)
|
this._dispatchInstructions(instruction)
|
||||||
break
|
break
|
||||||
@ -93,27 +84,28 @@ class JsonWsTunnel extends Guacamole.Tunnel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
private _disconnect() {
|
||||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
this.ws.close()
|
this.ws.close()
|
||||||
}
|
}
|
||||||
this.ws = null
|
this.ws = null
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(message: string) {
|
private _sendMessage(...elements: any[]) {
|
||||||
// Guacamole.Client calls this with raw Guacamole instruction strings.
|
// Guacamole.Client calls sendMessage(opcode, arg1, arg2, ...) with individual elements.
|
||||||
// Wrap in JSON envelope and forward to gateway → guacd.
|
// Encode into Guacamole wire format: "<len>.<value>,<len>.<value>,...;"
|
||||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
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
|
* Parse one or more Guacamole instructions from a raw buffer string
|
||||||
* and call oninstruction for each.
|
* and call oninstruction for each.
|
||||||
*
|
|
||||||
* Guacamole instruction format: "<len>.<opcode>,<len>.<arg1>,...;"
|
|
||||||
* Multiple instructions may arrive in one data chunk.
|
|
||||||
*/
|
*/
|
||||||
private _dispatchInstructions(raw: string) {
|
private _dispatchInstructions(raw: string) {
|
||||||
if (!this.oninstruction) return
|
if (!this.oninstruction) return
|
||||||
@ -121,14 +113,13 @@ class JsonWsTunnel extends Guacamole.Tunnel {
|
|||||||
let remaining = raw
|
let remaining = raw
|
||||||
while (remaining.length > 0) {
|
while (remaining.length > 0) {
|
||||||
const semicolonIdx = remaining.indexOf(';')
|
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)
|
const instruction = remaining.substring(0, semicolonIdx)
|
||||||
remaining = remaining.substring(semicolonIdx + 1)
|
remaining = remaining.substring(semicolonIdx + 1)
|
||||||
|
|
||||||
if (!instruction) continue
|
if (!instruction) continue
|
||||||
|
|
||||||
// Parse length-prefixed fields
|
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
let pos = 0
|
let pos = 0
|
||||||
while (pos < instruction.length) {
|
while (pos < instruction.length) {
|
||||||
@ -138,7 +129,7 @@ class JsonWsTunnel extends Guacamole.Tunnel {
|
|||||||
if (isNaN(len)) break
|
if (isNaN(len)) break
|
||||||
const value = instruction.substring(dotIdx + 1, dotIdx + 1 + len)
|
const value = instruction.substring(dotIdx + 1, dotIdx + 1 + len)
|
||||||
parts.push(value)
|
parts.push(value)
|
||||||
pos = dotIdx + 1 + len + 1 // skip comma separator
|
pos = dotIdx + 1 + len + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
@ -181,13 +172,10 @@ export function useRdp() {
|
|||||||
|
|
||||||
const client = new Guacamole.Client(tunnel)
|
const client = new Guacamole.Client(tunnel)
|
||||||
|
|
||||||
// Session ID is passed in by the caller (the pending session created in connectHost)
|
|
||||||
const sessionId = pendingSessionId
|
const sessionId = pendingSessionId
|
||||||
|
|
||||||
// Wire tunnel callbacks
|
|
||||||
tunnel.onConnected = (_resolvedHostId: number, resolvedHostName: string) => {
|
tunnel.onConnected = (_resolvedHostId: number, resolvedHostName: string) => {
|
||||||
console.log(`[RDP] Connected to ${resolvedHostName}`)
|
console.log(`[RDP] Connected to ${resolvedHostName}`)
|
||||||
// Update the pending session with the resolved host name
|
|
||||||
sessions.replaceSession(sessionId, {
|
sessions.replaceSession(sessionId, {
|
||||||
key: sessionId,
|
key: sessionId,
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
@ -222,13 +210,12 @@ export function useRdp() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Keyboard input — attach to document so key events fire even when
|
// Keyboard input
|
||||||
// focus is on other elements (toolbar buttons, etc.)
|
|
||||||
const keyboard = new Guacamole.Keyboard(document)
|
const keyboard = new Guacamole.Keyboard(document)
|
||||||
keyboard.onkeydown = (keysym: number) => client.sendKeyEvent(1, keysym)
|
keyboard.onkeydown = (keysym: number) => client.sendKeyEvent(1, keysym)
|
||||||
keyboard.onkeyup = (keysym: number) => client.sendKeyEvent(0, keysym)
|
keyboard.onkeyup = (keysym: number) => client.sendKeyEvent(0, keysym)
|
||||||
|
|
||||||
// Initiate the WebSocket connection (triggers connect handshake)
|
// Initiate the WebSocket connection
|
||||||
client.connect()
|
client.connect()
|
||||||
|
|
||||||
function disconnect() {
|
function disconnect() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user