openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
252 lines (251 loc) • 10.1 kB
JavaScript
import { a as logWarn, r as logInfo } from "./logger-lqqYRtFw.js";
import { f as isLoopbackIpAddress } from "./ip-0oQXo6_w.js";
import { a as stopActiveManagedProxyRegistration, i as registerActiveManagedProxyUrl, r as getActiveManagedProxyUrl, t as getActiveManagedProxyLoopbackMode } from "./active-proxy-state-DJLhrP_Z.js";
import { a as loadManagedProxyTlsOptionsSync, i as loadManagedProxyTlsOptions, o as resolveManagedProxyCaFileForUrl } from "./managed-proxy-undici-C8aIi6-p.js";
import { a as forceResetGlobalDispatcher } from "./undici-global-dispatcher-DTahNL39.js";
import { installGlobalProxy } from "@openclaw/proxyline";
//#region src/infra/net/proxy/proxy-lifecycle.ts
const PROXY_ENV_KEYS = [
"http_proxy",
"https_proxy",
"HTTP_PROXY",
"HTTPS_PROXY"
];
const NO_PROXY_ENV_KEYS = ["no_proxy", "NO_PROXY"];
const PROXY_ACTIVE_KEYS = [
"OPENCLAW_PROXY_ACTIVE",
"OPENCLAW_PROXY_LOOPBACK_MODE",
"OPENCLAW_PROXY_CA_FILE"
];
const ALL_PROXY_ENV_KEYS = [
...PROXY_ENV_KEYS,
...NO_PROXY_ENV_KEYS,
...PROXY_ACTIVE_KEYS
];
let baseProxyEnvSnapshot = null;
let proxylineHandle = null;
const MANAGED_PROXY_UNDICI_OPTIONS = Object.freeze({ allowH2: false });
/** Resets process-wide proxy lifecycle state between tests that share a worker. */
function resetProxyLifecycleForTests() {
baseProxyEnvSnapshot = null;
proxylineHandle?.stop();
proxylineHandle = null;
}
function captureProxyEnv() {
return {
http_proxy: process.env["http_proxy"],
https_proxy: process.env["https_proxy"],
HTTP_PROXY: process.env["HTTP_PROXY"],
HTTPS_PROXY: process.env["HTTPS_PROXY"],
no_proxy: process.env["no_proxy"],
NO_PROXY: process.env["NO_PROXY"],
OPENCLAW_PROXY_ACTIVE: process.env["OPENCLAW_PROXY_ACTIVE"],
OPENCLAW_PROXY_LOOPBACK_MODE: process.env["OPENCLAW_PROXY_LOOPBACK_MODE"],
OPENCLAW_PROXY_CA_FILE: process.env["OPENCLAW_PROXY_CA_FILE"]
};
}
function injectProxyEnv(proxyUrl, loopbackMode, proxyCaFile) {
const snapshot = captureProxyEnv();
applyProxyEnv(proxyUrl, loopbackMode, proxyCaFile);
return snapshot;
}
function applyProxyEnv(proxyUrl, loopbackMode, proxyCaFile) {
for (const key of PROXY_ENV_KEYS) process.env[key] = proxyUrl;
process.env["OPENCLAW_PROXY_ACTIVE"] = "1";
process.env["OPENCLAW_PROXY_LOOPBACK_MODE"] = loopbackMode;
if (proxyCaFile) process.env["OPENCLAW_PROXY_CA_FILE"] = proxyCaFile;
else delete process.env["OPENCLAW_PROXY_CA_FILE"];
for (const key of NO_PROXY_ENV_KEYS) process.env[key] = "";
}
function restoreProxyEnv(snapshot) {
for (const key of ALL_PROXY_ENV_KEYS) {
const value = snapshot[key];
if (value === void 0) delete process.env[key];
else process.env[key] = value;
}
}
function restoreInactiveProxyRuntime(snapshot) {
try {
proxylineHandle?.stop();
} catch (err) {
logWarn(`proxy: failed to stop Proxyline: ${String(err)}`);
}
proxylineHandle = null;
restoreProxyEnv(snapshot);
forceResetGlobalDispatcher();
ensureInheritedManagedProxyRoutingActive();
}
function restoreAfterFailedProxyActivation(restoreSnapshot) {
restoreInactiveProxyRuntime(restoreSnapshot);
baseProxyEnvSnapshot = null;
}
function stopActiveProxyRegistration(registration) {
if (registration.stopped) return;
stopActiveManagedProxyRegistration(registration);
if (getActiveManagedProxyUrl()) return;
const restoreSnapshot = baseProxyEnvSnapshot ?? captureProxyEnv();
baseProxyEnvSnapshot = null;
restoreInactiveProxyRuntime(restoreSnapshot);
}
function isSupportedProxyUrl(value) {
try {
const url = new URL(value);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}
function resolveProxyUrl(config) {
const candidate = config?.proxyUrl?.trim() || process.env["OPENCLAW_PROXY_URL"]?.trim();
if (!candidate) throw new Error("proxy: enabled but no HTTP proxy URL is configured; set proxy.proxyUrl or OPENCLAW_PROXY_URL to an http:// or https:// forward proxy.");
if (!isSupportedProxyUrl(candidate)) throw new Error("proxy: enabled but proxy URL is invalid; set proxy.proxyUrl or OPENCLAW_PROXY_URL to an http:// or https:// forward proxy.");
return candidate;
}
function redactProxyUrlForLog(value) {
try {
return new URL(value).origin;
} catch {
return "<invalid proxy URL>";
}
}
/** Reinstalls Proxyline routing in child processes that inherited active proxy env. */
function ensureInheritedManagedProxyRoutingActive() {
if (process.env["OPENCLAW_PROXY_ACTIVE"] !== "1") return;
const proxyUrl = process.env["HTTP_PROXY"];
if (!proxyUrl || !isSupportedProxyUrl(proxyUrl)) return;
const proxyTls = loadManagedProxyTlsOptionsSync(resolveManagedProxyCaFileForUrl({
proxyUrl,
caFileOverride: process.env["OPENCLAW_PROXY_CA_FILE"]
}));
proxylineHandle = installGlobalProxy({
mode: "managed",
proxyUrl,
...proxyTls ? { proxyTls } : {},
ifActive: "reuse-compatible",
undici: MANAGED_PROXY_UNDICI_OPTIONS
});
forceResetGlobalDispatcher({ preserveProxylineManaged: true });
}
/** Starts process-wide managed proxy routing and returns the owner stop handle. */
async function startProxy(config) {
if (config?.enabled !== true) return null;
const proxyUrl = resolveProxyUrl(config);
const loopbackMode = config.loopbackMode ?? "gateway-only";
const proxyCaFile = resolveManagedProxyCaFileForUrl({
proxyUrl,
config
});
const proxyTls = await loadManagedProxyTlsOptions(proxyCaFile);
if (getActiveManagedProxyUrl()) {
const registration = registerActiveManagedProxyUrl(new URL(proxyUrl), {
loopbackMode,
proxyTls
});
return {
proxyUrl,
stop: async () => {
stopActiveProxyRegistration(registration);
},
kill: () => {
stopActiveProxyRegistration(registration);
}
};
}
baseProxyEnvSnapshot ??= captureProxyEnv();
const lifecycleBaseEnvSnapshot = baseProxyEnvSnapshot;
let registration = null;
try {
injectProxyEnv(proxyUrl, loopbackMode, proxyCaFile);
proxylineHandle = installGlobalProxy({
mode: "managed",
proxyUrl,
...proxyTls ? { proxyTls } : {},
ifActive: "replace",
undici: MANAGED_PROXY_UNDICI_OPTIONS
});
forceResetGlobalDispatcher({ preserveProxylineManaged: true });
registration = registerActiveManagedProxyUrl(new URL(proxyUrl), {
loopbackMode,
proxyTls
});
} catch (err) {
if (registration) stopActiveManagedProxyRegistration(registration);
restoreAfterFailedProxyActivation(lifecycleBaseEnvSnapshot);
throw new Error(`proxy: failed to activate external proxy routing: ${String(err)}`, { cause: err });
}
logInfo(`proxy: routing process HTTP traffic through external proxy ${redactProxyUrlForLog(proxyUrl)}`);
return {
proxyUrl,
stop: async () => {
if (registration) stopActiveProxyRegistration(registration);
},
kill: () => {
if (registration) stopActiveProxyRegistration(registration);
}
};
}
/** Stops a managed proxy handle if one was started. */
async function stopProxy(handle) {
if (!handle) return;
await handle.stop();
}
function parseGatewayControlPlaneUrl(value) {
try {
return new URL(value);
} catch {
return null;
}
}
function isGatewayControlPlaneProtocol(protocol) {
return protocol === "ws:" || protocol === "wss:" || protocol === "http:" || protocol === "https:";
}
function getGatewayControlPlaneBypassAuthority(value) {
const url = parseGatewayControlPlaneUrl(value);
if (url === null || !isGatewayControlPlaneProtocol(url.protocol) || !isGatewayControlPlaneLoopbackHost(url.hostname)) return null;
return url.port ? `${url.hostname}:${url.port}` : url.hostname;
}
/** Registers a temporary direct route for trusted Gateway loopback control-plane URLs. */
function registerManagedProxyGatewayLoopbackBypass(url) {
if (!getGatewayControlPlaneBypassAuthority(url)) return;
const loopbackMode = getActiveManagedProxyLoopbackMode();
if (loopbackMode === "block") throw new Error("proxy: Gateway loopback control-plane connections are blocked by proxy.loopbackMode");
if (loopbackMode === "proxy") return;
return proxylineHandle?.registerBypass({ url });
}
function isGatewayControlPlaneLoopbackHost(hostname) {
return hostname.trim().toLowerCase().replace(/\.+$/, "") === "localhost" || isLoopbackIpAddress(hostname);
}
/**
* Carve out the operator-managed external proxy for the Browser plugin's
* loopback CDP probe to a Chromium instance OpenClaw spawned itself.
*
* The managed proxy installs a process-wide undici dispatcher that would
* otherwise route `http://127.0.0.1:<cdpPort>/json/version` and the
* `ws://127.0.0.1:<cdpPort>/devtools/...` upgrade through the external
* forward proxy, which returns 502 because nothing on the proxy listens for
* the loopback CDP port. The bypass restores direct loopback delivery for
* the duration the caller holds the returned `unregister` callback.
*
* Loopback-gated by structure: non-loopback authorities (e.g. an `attachOnly`
* profile pointing at a remote CDP service like Browserless/Browserbase) are
* not bypassed and continue to traverse the external proxy as configured.
*
* Honors `proxy.loopbackMode`:
* - `gateway-only` (default): register the bypass.
* - `proxy`: do not bypass — operator opted into proxy-everything routing.
* - `block`: throw — operator forbids loopback IPC under managed proxy.
*
* Note: A loopback `attachOnly` profile whose `cdpUrl` is e.g.
* `http://127.0.0.1:<port>` would also satisfy this gate. This mirrors the
* structural semantics of `registerManagedProxyGatewayLoopbackBypass` —
* loopback IPC on this host is assumed to be operator-trusted.
*/
function registerManagedProxyBrowserCdpBypass(url) {
if (!getGatewayControlPlaneBypassAuthority(url)) return;
const loopbackMode = getActiveManagedProxyLoopbackMode();
if (loopbackMode === "block") throw new Error("proxy: Browser loopback CDP connections are blocked by proxy.loopbackMode");
if (loopbackMode === "proxy") return;
return proxylineHandle?.registerBypass({ url });
}
//#endregion
export { startProxy as a, resetProxyLifecycleForTests as i, registerManagedProxyBrowserCdpBypass as n, stopProxy as o, registerManagedProxyGatewayLoopbackBypass as r, ensureInheritedManagedProxyRoutingActive as t };