UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

160 lines (159 loc) 7.14 kB
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { t as safeEqualSecret } from "./secret-equal-DRsL8lKD.js"; import { a as getPluginRuntimeGatewayRequestScope } from "./gateway-request-scope-BCMYlsDI.js"; import "./plugin-runtime-Cl_BX05Z.js"; import "./security-runtime-Ckf0kc0h.js"; import { a as resolveProfile } from "./config-BosO4Zbt.js"; import { o as getProfileLifecycle } from "./server-context.lifecycle-Tmt_A8dN.js"; import "./subsystem-DbD06g1M.js"; import "./config-C0cjs7WS.js"; import { r as getBrowserControlState } from "./browser-control-state-BwkjlfAE.js"; import { t as startBrowserControlServiceFromConfig } from "./control-service-DCNNOWdd.js"; import { n as readExtensionRelayToken } from "./relay-auth-DPRfu8tu.js"; import { a as getBrowserRelayAuthV2Authority, o as invalidateBrowserRelayAuthV2Authority, s as parseExtensionRelayResource } from "./auth-v2-B-IXoFi_.js"; import { a as isAllowedExtensionOrigin, h as handlePreAuthWebSocketUpgrade, m as authenticateExtensionWebSocket, n as attachExtensionWebSocket, o as requestExtensionProtocolToken, s as requestProtocols, t as EXTENSION_RELAY_MAX_PAYLOAD_BYTES } from "./relay-server-9erhkSvY.js"; import { n as ensureExtensionRelayForProfile } from "./relay-lifecycle-CI1DUkoi.js"; import { WebSocketServer } from "ws"; //#region extensions/browser/src/browser/extension-relay/gateway-relay-route.ts const log = createSubsystemLogger("browser").child("extension-relay-gateway"); const GATEWAY_EXTENSION_RELAY_PATH = "/browser/extension"; let wss = null; function getWss() { wss ??= new WebSocketServer({ noServer: true, maxPayload: EXTENSION_RELAY_MAX_PAYLOAD_BYTES }); return wss; } function destroy(socket, statusLine) { try { socket.write(`HTTP/1.1 ${statusLine}\r\nConnection: close\r\n\r\n`); } finally { socket.destroy(); } } function requestedProfileName(resource, fallback) { return new URL(resource, "http://127.0.0.1").searchParams.get("profile") ?? fallback; } function defaultExtensionProfileName(profiles) { for (const [name, profile] of Object.entries(profiles)) if (profile.driver === "extension") return name; return "chrome"; } async function resolveGatewayRelay(resource) { let state = getBrowserControlState(); if (!state) { state = await startBrowserControlServiceFromConfig(); if (!state) throw new Error("Browser control is disabled"); } const profileName = requestedProfileName(resource, defaultExtensionProfileName(state.resolved.profiles)); const resolved = resolveProfile(state.resolved, profileName); if (!resolved || resolved.driver !== "extension") throw new Error(`Extension browser profile "${profileName}" was not found`); const relay = await ensureExtensionRelayForProfile(state, resolved); const runtime = state.profiles.get(profileName); if (!runtime) throw new Error("Gateway relay profile is unavailable"); const lifecycle = getProfileLifecycle(runtime); const generation = lifecycle.generation; return { relay, profileName, assertCurrent: () => { if (lifecycle.generation !== generation || lifecycle.transitionReason || lifecycle.terminal || getBrowserControlState() !== state || state.extensionRelays?.get(profileName) !== relay || state.profiles.get(profileName) !== runtime || readExtensionRelayToken() !== relay.token || resolveProfile(state.resolved, profileName)?.cdpPort !== relay.port) throw new Error("Gateway relay ingress was superseded"); } }; } async function prepareGatewayIngress(access, ws) { access.assertCurrent(); const relay = access.relay; const attach = relay.ownership === "borrowed" ? await relay.client.prepareIngress(ws) : () => attachExtensionWebSocket(relay.bridge, ws); return () => { access.assertCurrent(); attach(); log.info(`extension connected over gateway for profile "${access.profileName}"`); }; } /** Handle the plugin-owned Gateway upgrade path. */ async function handleGatewayExtensionUpgrade(req, socket, head) { const resource = parseExtensionRelayResource(req.url ?? "/", GATEWAY_EXTENSION_RELAY_PATH); if (!resource) return (req.url ?? "/").split("?")[0] === GATEWAY_EXTENSION_RELAY_PATH ? (destroy(socket, "400 Bad Request"), true) : false; if (!isAllowedExtensionOrigin(req)) { destroy(socket, "403 Forbidden"); return true; } const protocols = requestProtocols(req); const source = getPluginRuntimeGatewayRequestScope()?.client?.clientIp ?? req.socket?.remoteAddress ?? "unknown"; const token = readExtensionRelayToken(); if (!token) { invalidateBrowserRelayAuthV2Authority(); destroy(socket, "401 Unauthorized"); return true; } if (protocols.length === 1 && protocols[0] === "openclaw-extension-relay.v2") { const authority = getBrowserRelayAuthV2Authority(token); if (!handlePreAuthWebSocketUpgrade({ wss: getWss(), req, socket, head, onUpgrade: (ws, removePreAuthGuard) => { authenticateExtensionWebSocket({ ws, authority, source, resource, removePreAuthGuard, prepareAuthenticated: async () => { if (readExtensionRelayToken() !== token) throw new Error("browser relay key rotated during authentication"); const attach = await prepareGatewayIngress(await resolveGatewayRelay(resource), ws); return () => { if (readExtensionRelayToken() !== token) throw new Error("Relay key rotated"); attach(); }; } }); } })) destroy(socket, "400 Bad Request"); return true; } if (protocols.includes("openclaw-extension-relay.v2")) { destroy(socket, "400 Bad Request"); return true; } const allowLegacyAuth = getRuntimeConfig().browser?.extensionRelay?.allowLegacyAuth !== false; const legacyToken = requestExtensionProtocolToken(req); if (!allowLegacyAuth || !protocols.includes("openclaw-extension-relay") || legacyToken.length === 0 || !safeEqualSecret(token, legacyToken)) { destroy(socket, "401 Unauthorized"); return true; } let resolved; try { resolved = await resolveGatewayRelay(resource); } catch (err) { log.warn(`failed to start Browser control for legacy extension relay: ${String(err)}`); destroy(socket, "503 Service Unavailable"); return true; } const authority = getBrowserRelayAuthV2Authority(token); getWss().handleUpgrade(req, socket, head, (ws) => { ws.pause(); ws.on("error", (err) => log.warn(`relay socket error: ${String(err)}`)); if (!authority.registerAuthenticatedConnection(ws, () => ws.close(4003, "browser relay key rotated"))) { ws.terminate(); return; } ws.once("close", () => authority.releaseConnection(ws)); prepareGatewayIngress(resolved, ws).then((attach) => { if (ws.readyState === 1) attach(); }).catch(() => ws.close(1011, "Relay ingress unavailable")).finally(() => ws.resume()); log.warn(`legacy extension authentication accepted for profile "${resolved.profileName}"`); }); return true; } function disposeGatewayExtensionRelay() { if (!wss) return; for (const client of wss.clients) client.terminate(); wss.close(); wss = null; } //#endregion export { disposeGatewayExtensionRelay, handleGatewayExtensionUpgrade };