fix(rdp): replace class extends with direct instance method override on Guacamole.Tunnel
This commit is contained in:
parent
76db0a6936
commit
72526487c3
@ -3,123 +3,29 @@ import { useAuthStore } from '~/stores/auth.store'
|
|||||||
import { useSessionStore } from '~/stores/session.store'
|
import { useSessionStore } from '~/stores/session.store'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Guacamole Tunnel that speaks JSON over WebSocket.
|
* Creates a Guacamole-compatible tunnel that speaks JSON over WebSocket.
|
||||||
*
|
* Does NOT extend Guacamole.Tunnel (its constructor assigns no-op instance
|
||||||
* Why custom instead of Guacamole.WebSocketTunnel:
|
* properties that shadow subclass methods). Instead, creates a base tunnel
|
||||||
* - The standard WebSocketTunnel expects raw Guacamole protocol over WS.
|
* and overwrites the methods directly.
|
||||||
* - Our /ws/rdp gateway wraps all messages in JSON envelopes (same pattern
|
|
||||||
* as /ws/terminal and /ws/sftp) for auth, connect handshake, and error
|
|
||||||
* signaling.
|
|
||||||
* - This tunnel bridges the two: guacamole-common-js sees raw Guacamole
|
|
||||||
* instructions while our gateway sees consistent JSON messages.
|
|
||||||
*
|
|
||||||
* 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 {
|
function createJsonWsTunnel(wsUrl: string, connectMsg: object) {
|
||||||
private ws: WebSocket | null = null
|
const tunnel = new Guacamole.Tunnel()
|
||||||
private readonly wsUrl: string
|
let ws: WebSocket | null = null
|
||||||
private readonly connectMsg: object
|
|
||||||
|
|
||||||
// Expose for external callers (connected event, disconnect action)
|
// Custom callbacks for our JSON protocol layer
|
||||||
onConnected: ((hostId: number, hostName: string) => void) | null = null
|
let onConnected: ((hostId: number, hostName: string) => void) | null = null
|
||||||
onDisconnected: ((reason: string) => void) | null = null
|
let onDisconnected: ((reason: string) => void) | null = null
|
||||||
onGatewayError: ((message: string) => void) | null = null
|
let onGatewayError: ((message: string) => void) | null = null
|
||||||
|
|
||||||
constructor(wsUrl: string, connectMsg: object) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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')
|
|
||||||
this.ws!.send(JSON.stringify(this.connectMsg))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ws.onmessage = (event: MessageEvent) => {
|
|
||||||
const msg = JSON.parse(event.data as string)
|
|
||||||
|
|
||||||
switch (msg.type) {
|
|
||||||
case 'connected':
|
|
||||||
this.onConnected?.(msg.hostId, msg.hostName)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'guac': {
|
|
||||||
const instruction: string = msg.instruction
|
|
||||||
this._dispatchInstructions(instruction)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'error':
|
|
||||||
this.onGatewayError?.(msg.message as string)
|
|
||||||
this.onerror?.(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, msg.message))
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'disconnected':
|
|
||||||
this.onDisconnected?.(msg.reason as string)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ws.onclose = (event: CloseEvent) => {
|
|
||||||
if (event.code === 4001) {
|
|
||||||
this.onerror?.(new Guacamole.Status(Guacamole.Status.Code.CLIENT_FORBIDDEN, 'Unauthorized'))
|
|
||||||
}
|
|
||||||
this.onDisconnected?.('WebSocket closed')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ws.onerror = () => {
|
|
||||||
this.onerror?.(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, 'WebSocket error'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _disconnect() {
|
|
||||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
||||||
this.ws.close()
|
|
||||||
}
|
|
||||||
this.ws = null
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
private _dispatchInstructions(raw: string) {
|
|
||||||
if (!this.oninstruction) return
|
|
||||||
|
|
||||||
|
function dispatchInstructions(raw: string) {
|
||||||
|
if (!tunnel.oninstruction) return
|
||||||
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
|
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
|
||||||
|
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
let pos = 0
|
let pos = 0
|
||||||
while (pos < instruction.length) {
|
while (pos < instruction.length) {
|
||||||
@ -127,18 +33,84 @@ class JsonWsTunnel extends Guacamole.Tunnel {
|
|||||||
if (dotIdx === -1) break
|
if (dotIdx === -1) break
|
||||||
const len = parseInt(instruction.substring(pos, dotIdx), 10)
|
const len = parseInt(instruction.substring(pos, dotIdx), 10)
|
||||||
if (isNaN(len)) break
|
if (isNaN(len)) break
|
||||||
const value = instruction.substring(dotIdx + 1, dotIdx + 1 + len)
|
parts.push(instruction.substring(dotIdx + 1, dotIdx + 1 + len))
|
||||||
parts.push(value)
|
|
||||||
pos = dotIdx + 1 + len + 1
|
pos = dotIdx + 1 + len + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
const opcode = parts[0]
|
tunnel.oninstruction(parts[0], parts.slice(1))
|
||||||
const args = parts.slice(1)
|
|
||||||
this.oninstruction(opcode, args)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override the no-op instance methods
|
||||||
|
tunnel.connect = (_data?: string) => {
|
||||||
|
console.log('[RDP] Tunnel opening WebSocket:', wsUrl)
|
||||||
|
ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('[RDP] WebSocket open, sending connect handshake')
|
||||||
|
ws!.send(JSON.stringify(connectMsg))
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (event: MessageEvent) => {
|
||||||
|
const msg = JSON.parse(event.data as string)
|
||||||
|
switch (msg.type) {
|
||||||
|
case 'connected':
|
||||||
|
onConnected?.(msg.hostId, msg.hostName)
|
||||||
|
break
|
||||||
|
case 'guac':
|
||||||
|
dispatchInstructions(msg.instruction)
|
||||||
|
break
|
||||||
|
case 'error':
|
||||||
|
onGatewayError?.(msg.message)
|
||||||
|
tunnel.onerror?.(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, msg.message))
|
||||||
|
break
|
||||||
|
case 'disconnected':
|
||||||
|
onDisconnected?.(msg.reason)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = (event: CloseEvent) => {
|
||||||
|
console.log('[RDP] WebSocket closed, code:', event.code)
|
||||||
|
if (event.code === 4001) {
|
||||||
|
tunnel.onerror?.(new Guacamole.Status(Guacamole.Status.Code.CLIENT_FORBIDDEN, 'Unauthorized'))
|
||||||
|
}
|
||||||
|
onDisconnected?.('WebSocket closed')
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = () => {
|
||||||
|
console.error('[RDP] WebSocket error')
|
||||||
|
tunnel.onerror?.(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, 'WebSocket error'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel.disconnect = () => {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close()
|
||||||
|
}
|
||||||
|
ws = null
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel.sendMessage = (...elements: any[]) => {
|
||||||
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
|
const instruction = elements.map((e: any) => {
|
||||||
|
const s = String(e)
|
||||||
|
return `${s.length}.${s}`
|
||||||
|
}).join(',') + ';'
|
||||||
|
ws.send(JSON.stringify({ type: 'guac', instruction }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tunnel,
|
||||||
|
get onConnected() { return onConnected },
|
||||||
|
set onConnected(fn) { onConnected = fn },
|
||||||
|
get onDisconnected() { return onDisconnected },
|
||||||
|
set onDisconnected(fn) { onDisconnected = fn },
|
||||||
|
get onGatewayError() { return onGatewayError },
|
||||||
|
set onGatewayError(fn) { onGatewayError = fn },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Composable ────────────────────────────────────────────────────────────────
|
// ─── Composable ────────────────────────────────────────────────────────────────
|
||||||
@ -163,18 +135,17 @@ export function useRdp() {
|
|||||||
|
|
||||||
console.log(`[RDP] Connecting to ${wsUrl} for hostId=${hostId} (${width}x${height})`)
|
console.log(`[RDP] Connecting to ${wsUrl} for hostId=${hostId} (${width}x${height})`)
|
||||||
|
|
||||||
const tunnel = new JsonWsTunnel(wsUrl, {
|
const wrapper = createJsonWsTunnel(wsUrl, {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
hostId,
|
hostId,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
})
|
})
|
||||||
|
|
||||||
const client = new Guacamole.Client(tunnel)
|
const client = new Guacamole.Client(wrapper.tunnel)
|
||||||
|
|
||||||
const sessionId = pendingSessionId
|
const sessionId = pendingSessionId
|
||||||
|
|
||||||
tunnel.onConnected = (_resolvedHostId: number, resolvedHostName: string) => {
|
wrapper.onConnected = (_resolvedHostId: number, resolvedHostName: string) => {
|
||||||
console.log(`[RDP] Connected to ${resolvedHostName}`)
|
console.log(`[RDP] Connected to ${resolvedHostName}`)
|
||||||
sessions.replaceSession(sessionId, {
|
sessions.replaceSession(sessionId, {
|
||||||
key: sessionId,
|
key: sessionId,
|
||||||
@ -187,13 +158,13 @@ export function useRdp() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnel.onDisconnected = (reason: string) => {
|
wrapper.onDisconnected = (reason: string) => {
|
||||||
console.log(`[RDP] Disconnected: ${reason}`)
|
console.log(`[RDP] Disconnected: ${reason}`)
|
||||||
sessions.removeSession(sessionId)
|
sessions.removeSession(sessionId)
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnel.onGatewayError = (message: string) => {
|
wrapper.onGatewayError = (message: string) => {
|
||||||
console.error('[RDP] Gateway error:', message)
|
console.error('[RDP] Gateway error:', message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user