UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

249 lines (248 loc) 8.87 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { i as registerSecretValueForRedaction } from "./secret-redaction-registry-BOLC6DkF.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { r as NODE_PORTAL_ATTACH_PATH, t as NODE_DESKTOP_ATTACH_PATH } from "./node-desktop-stream-BZM2AiRA.js"; import { t as startWebSocketKeepalive } from "./websocket-keepalive-Jg8mBtsJ.js"; import crypto from "node:crypto"; import { WebSocketServer, createWebSocketStream } from "ws"; //#region src/gateway/desktop/node-stream-broker.ts const DEFAULT_TICKET_TTL_MS = 6e4; const TICKET_PATTERN = /^[a-f0-9]{48}$/u; const MAX_ATTACH_FRAME_BYTES = 65536; const streamLog = createSubsystemLogger("gateway/node-stream"); function rawDataBuffer(data) { if (Buffer.isBuffer(data)) return data; if (Array.isArray(data)) return Buffer.concat(data); return Buffer.from(data); } function parseStreamMetadata(data, isBinary, kind) { const buffer = rawDataBuffer(data); if (!isBinary || buffer.length === 0 || buffer.length > MAX_ATTACH_FRAME_BYTES) throw new Error(`invalid node ${kind} attach metadata`); let value; try { value = JSON.parse(buffer.toString("utf8")); } catch { throw new Error(`invalid node ${kind} attach metadata`); } if (kind === "portal") { if (!isRecord(value) || value.ok !== true || Object.keys(value).length !== 1) throw new Error("invalid node portal attach metadata"); return; } if (!isRecord(value) || value.auth !== "vnc-password" && value.auth !== "ard-account") throw new Error("invalid node desktop attach metadata"); if (Object.keys(value).some((key) => key !== "auth" && key !== "vncPassword")) throw new Error("invalid node desktop attach metadata"); if (value.vncPassword !== void 0 && typeof value.vncPassword !== "string") throw new Error("invalid node desktop attach metadata"); if (value.auth === "ard-account" && value.vncPassword !== void 0) throw new Error("invalid node desktop attach metadata"); const vncPassword = typeof value.vncPassword === "string" ? value.vncPassword : void 0; if (vncPassword) registerSecretValueForRedaction(vncPassword); return { auth: value.auth, ...vncPassword ? { vncPassword } : {} }; } function writeUnauthorized(socket) { socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n"); socket.destroy(); } function readAttachedStream(ws, kind, onStreamError) { return new Promise((resolve, reject) => { const cleanup = () => { ws.off("message", onMessage); ws.off("close", onClose); }; const onMessage = (data, isBinary) => { cleanup(); try { const metadata = parseStreamMetadata(data, isBinary, kind); const stream = createWebSocketStream(ws, { allowHalfOpen: false }); stream.on("error", onStreamError); resolve({ metadata, stream }); } catch (error) { reject(error instanceof Error ? error : new Error(String(error))); } }; const onClose = () => { cleanup(); reject(/* @__PURE__ */ new Error(`node ${kind} stream closed before attach`)); }; const onError = (error) => { cleanup(); onStreamError(error); reject(error); }; ws.once("message", onMessage); ws.once("close", onClose); ws.on("error", onError); }); } /** Owns one-time node stream tickets and pairs authenticated desktop or portal duplexes. */ function createNodeDesktopStreamBroker(deps = {}) { const ttlMs = deps.ttlMs ?? DEFAULT_TICKET_TTL_MS; const now = deps.now ?? Date.now; const tickets = /* @__PURE__ */ new Map(); const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_ATTACH_FRAME_BYTES }); const remove = (ticket) => { const entry = tickets.get(ticket); if (!entry) return; tickets.delete(ticket); clearTimeout(entry.timer); return entry; }; const rejectTicket = (ticket, error) => { const entry = remove(ticket); if (!entry) return; entry.reject(error); entry.stopKeepalive?.(); entry.closeTrigger ??= "attach-rejected"; entry.ws?.close(1008, `node ${entry.kind} attach rejected`); entry.socket?.destroy(); }; const resolveTicket = (ticket, stream, metadata) => { const entry = remove(ticket); if (!entry) { stream.destroy(); return; } entry.resolve(stream, metadata); }; function mintStream(binding, kind, attach) { const ticket = crypto.randomBytes(24).toString("hex"); const expiresAtMs = now() + ttlMs; let resolve; let reject; const attached = new Promise((resolvePromise, rejectPromise) => { resolve = (stream, metadata) => { try { resolvePromise(attach(stream, metadata)); } catch (error) { stream.destroy(); rejectPromise(error instanceof Error ? error : new Error(String(error))); } }; reject = rejectPromise; }); attached.catch(() => void 0); const timer = setTimeout(() => { rejectTicket(ticket, /* @__PURE__ */ new Error(`node ${kind} stream ticket expired`)); }, ttlMs); timer.unref?.(); tickets.set(ticket, { kind, binding, expiresAtMs, resolve, reject, timer, redeemed: false }); return { ticket, attachPath: `${kind === "desktop" ? NODE_DESKTOP_ATTACH_PATH : NODE_PORTAL_ATTACH_PATH}?ticket=${ticket}`, expiresAtMs, attached, cancel() { rejectTicket(ticket, /* @__PURE__ */ new Error(`node ${kind} stream ticket cancelled`)); } }; } function mint(binding) { return mintStream(binding, "desktop", (stream, metadata) => { if (!metadata) throw new Error("invalid node desktop attach metadata"); return { ...metadata, stream }; }); } function mintPortal(binding) { return mintStream(binding, "portal", (stream) => ({ stream })); } const bindingIsCurrent = async (registry, binding) => { const current = registry.getForPairingGeneration(binding.nodeId, binding.pairingGeneration); if (!current || current.connId !== binding.connId) return false; if (!await registry.isConnectionCurrentPairingState(binding.connId)) return false; return registry.getForPairingGeneration(binding.nodeId, binding.pairingGeneration)?.connId === binding.connId; }; async function handleUpgrade(req, socket, head, registry) { const resource = new URL(req.url ?? "/", "http://127.0.0.1"); const kind = resource.pathname === "/node-desktop/attach" ? "desktop" : resource.pathname === "/node-portal/attach" ? "portal" : void 0; if (!kind) return false; const ticket = (resource.searchParams.get("ticket") ?? "").trim(); if (!TICKET_PATTERN.test(ticket)) { writeUnauthorized(socket); return true; } const entry = tickets.get(ticket); if (!entry || entry.kind !== kind || entry.redeemed || entry.expiresAtMs <= now()) { writeUnauthorized(socket); if (entry && entry.kind === kind && !entry.redeemed) rejectTicket(ticket, /* @__PURE__ */ new Error(`node ${kind} stream ticket expired`)); return true; } entry.redeemed = true; entry.socket = socket; const onSocketError = (error) => rejectTicket(ticket, error); const onSocketClose = () => rejectTicket(ticket, /* @__PURE__ */ new Error(`node ${kind} attach closed during authorization`)); socket.once("error", onSocketError); socket.once("end", onSocketClose); socket.once("close", onSocketClose); let current; try { current = await bindingIsCurrent(registry, entry.binding); } catch { current = false; } if (tickets.get(ticket) !== entry) return true; socket.off("error", onSocketError); socket.off("end", onSocketClose); socket.off("close", onSocketClose); if (!current) { writeUnauthorized(socket); rejectTicket(ticket, /* @__PURE__ */ new Error(`node ${kind} stream ticket binding is stale`)); return true; } try { wss.handleUpgrade(req, socket, head, (ws) => { entry.socket = void 0; entry.ws = ws; ws.once("close", (closeCode) => { streamLog.info("node stream closed", { streamKind: kind, nodeId: entry.binding.nodeId, connId: entry.binding.connId, trigger: entry.closeTrigger ?? "websocket-close", closeCode }); }); ws.once("error", () => { entry.closeTrigger ??= "websocket-error"; }); entry.stopKeepalive = startWebSocketKeepalive(ws); const attached = readAttachedStream(ws, kind, (error) => rejectTicket(ticket, error)); (async () => { try { const resolved = await attached; if (!await bindingIsCurrent(registry, entry.binding)) throw new Error(`node ${kind} stream ticket binding is stale`); resolveTicket(ticket, resolved.stream, resolved.metadata); } catch (error) { rejectTicket(ticket, error instanceof Error ? error : new Error(String(error))); } })(); }); } catch (error) { rejectTicket(ticket, error instanceof Error ? error : new Error(String(error))); } return true; } return { mint, mintPortal, handleUpgrade }; } //#endregion export { createNodeDesktopStreamBroker };