openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
160 lines (159 loc) • 7.14 kB
JavaScript
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 };