UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

247 lines (246 loc) 7.51 kB
import net from "node:net"; import { Duplex } from "node:stream"; //#region src/gateway/desktop/rfb-probe.ts const RFB_BANNER_BYTES = 12; const RFB_37_MINOR = 7; const RFB_37_BANNER = Buffer.from("RFB 003.007\n", "ascii"); const RFB_38_BANNER = Buffer.from("RFB 003.008\n", "ascii"); /** Parses the fixed-width RFB ProtocolVersion banner without socket state. */ function parseRfbVersionBanner(buffer) { const banner = buffer.subarray(0, RFB_BANNER_BYTES).toString("ascii"); if (buffer.length < RFB_BANNER_BYTES) return { kind: "not-rfb", banner }; const match = /^RFB 003\.(\d{3})\n$/u.exec(banner); if (!match) return { kind: "not-rfb", banner }; const minor = Number.parseInt(match[1] ?? "", 10); return { kind: "rfb", minor, reply: minor > RFB_37_MINOR ? RFB_38_BANNER : minor === RFB_37_MINOR ? RFB_37_BANNER : Buffer.from("RFB 003.003\n", "ascii") }; } /** Parses the post-version RFB security offer from a standalone buffer. */ function parseRfbSecurityTypes(buffer, protocolMinor) { if (protocolMinor < RFB_37_MINOR) { if (buffer.length < 4) return { kind: "incomplete", requiredBytes: 4 }; const securityType = buffer.readUInt32BE(0); return { kind: "complete", securityTypes: securityType === 0 ? [] : [securityType] }; } if (buffer.length < 1) return { kind: "incomplete", requiredBytes: 1 }; const count = buffer.readUInt8(0); if (count > 0) { const requiredBytes = 1 + count; return buffer.length < requiredBytes ? { kind: "incomplete", requiredBytes } : { kind: "complete", securityTypes: [...buffer.subarray(1, requiredBytes)] }; } return { kind: "complete", securityTypes: [] }; } var SocketEndedError = class extends Error { constructor(buffered) { super("RFB server closed the handshake early"); this.buffered = buffered; } }; var SocketTimeoutError = class extends Error {}; function createSocketReader(socket) { let failure; const waiters = /* @__PURE__ */ new Set(); const wake = () => { for (const waiter of waiters) waiter(); waiters.clear(); }; const onEnd = () => { failure ??= new SocketEndedError(socket.read() ?? Buffer.alloc(0)); wake(); }; const onError = (error) => { failure = error; wake(); }; socket.on("readable", wake); socket.once("end", onEnd); socket.once("close", onEnd); socket.once("error", onError); return { async readExactly(length) { for (;;) { const value = socket.read(length); if (value) { if (value.length !== length) throw new SocketEndedError(value); return value; } if (failure) throw failure; await new Promise((resolve) => { waiters.add(resolve); }); } }, detach() { socket.pause(); socket.off("readable", wake); socket.off("end", onEnd); socket.off("close", onEnd); socket.off("error", onError); } }; } /** Replays the inspected prefix while keeping authentication on the same TCP connection. */ var ConnectedRfbStream = class extends Duplex { constructor(socket, versionReply) { super({ allowHalfOpen: false }); this.socket = socket; this.versionReply = versionReply; this.version = Buffer.alloc(0); this.on("error", () => void 0); socket.on("data", (chunk) => { if (!this.push(chunk)) socket.pause(); }); socket.once("end", () => { this.push(null); this.end(); }); socket.once("close", () => { if (!socket.readableEnded) this.destroy(/* @__PURE__ */ new Error("RFB peer closed before ending the stream")); }); socket.once("error", (error) => this.destroy(error)); } _read() { this.socket.resume(); } _write(chunk, _encoding, callback) { let remaining = chunk; if (this.version.length < RFB_BANNER_BYTES) { const consumed = Math.min(RFB_BANNER_BYTES - this.version.length, chunk.length); this.version = Buffer.concat([this.version, chunk.subarray(0, consumed)]); remaining = chunk.subarray(consumed); if (this.version.length === RFB_BANNER_BYTES && !this.version.equals(this.versionReply)) { callback(/* @__PURE__ */ new Error("RFB client changed the inspected protocol version")); return; } } if (remaining.length) this.socket.write(remaining, callback); else callback(); } _final(callback) { this.socket.end(callback); } _destroy(error, callback) { this.socket.destroy(); callback(error); } }; /** Inspects an RFB offer and retains that connection for its authenticated stream. */ async function connectRfbServer(params) { params.signal?.throwIfAborted(); const socket = net.createConnection({ port: params.port, host: params.host, allowHalfOpen: true }); const deadline = setTimeout(() => { socket.destroy(new SocketTimeoutError("RFB handshake timed out")); }, params.timeoutMs); deadline.unref(); const reader = createSocketReader(socket); let retained = false; const onAbort = () => socket.destroy(params.signal?.reason instanceof Error ? params.signal.reason : /* @__PURE__ */ new Error("RFB connection aborted")); params.signal?.addEventListener("abort", onAbort, { once: true }); socket.once("close", () => params.signal?.removeEventListener("abort", onAbort)); try { await new Promise((resolve, reject) => { socket.once("connect", resolve); socket.once("error", reject); }); let bannerBytes; try { bannerBytes = await reader.readExactly(RFB_BANNER_BYTES); } catch (error) { params.signal?.throwIfAborted(); if (error instanceof SocketEndedError) return { kind: "not-rfb", banner: error.buffered.toString("ascii") }; throw error; } const version = parseRfbVersionBanner(bannerBytes); if (version.kind === "not-rfb") return version; socket.write(version.reply); const prefixBytes = version.minor < RFB_37_MINOR ? 4 : 1; let securityBuffer = await reader.readExactly(prefixBytes); let parsed = parseRfbSecurityTypes(securityBuffer, version.minor); while (parsed.kind === "incomplete") { securityBuffer = Buffer.concat([securityBuffer, await reader.readExactly(parsed.requiredBytes - securityBuffer.length)]); parsed = parseRfbSecurityTypes(securityBuffer, version.minor); } params.signal?.throwIfAborted(); reader.detach(); const stream = new ConnectedRfbStream(socket, version.reply); socket.unshift(Buffer.concat([bannerBytes, securityBuffer])); retained = true; return { kind: "rfb", securityTypes: parsed.securityTypes, stream }; } catch (error) { params.signal?.throwIfAborted(); if (error instanceof SocketTimeoutError) return { kind: "timeout" }; return { kind: "unreachable" }; } finally { clearTimeout(deadline); if (!retained) { reader.detach(); socket.destroy(); } } } /** Status-only inspection; streaming callers retain the connected result instead. */ async function probeRfbServer(params) { const result = await connectRfbServer(params); if (result.kind !== "rfb") return result; result.stream.destroy(); return { kind: "rfb", securityTypes: result.securityTypes }; } /** Maps standard RFB security numbers into the credential UX supported by OpenClaw. */ function classifyRfbSecurity(securityTypes) { for (const securityType of securityTypes) { if (securityType === 1) return "none"; if (securityType === 2) return "vnc-password"; if (securityType === 30) return "ard-account"; if ([ 6, 16, 19, 22, 113 ].includes(securityType)) return "unsupported"; } return "unsupported"; } //#endregion export { probeRfbServer as i, connectRfbServer as n, parseRfbVersionBanner as r, classifyRfbSecurity as t };