mirror of https://github.com/openclaw/openclaw.git
152 lines
4.2 KiB
TypeScript
152 lines
4.2 KiB
TypeScript
/**
|
|
* Proxy bypass for CDP (Chrome DevTools Protocol) localhost connections.
|
|
*
|
|
* When HTTP_PROXY / HTTPS_PROXY / ALL_PROXY environment variables are set,
|
|
* CDP connections to localhost/127.0.0.1 can be incorrectly routed through
|
|
* the proxy, causing browser control to fail.
|
|
*
|
|
* @see https://github.com/nicepkg/openclaw/issues/31219
|
|
*/
|
|
import http from "node:http";
|
|
import https from "node:https";
|
|
import { isLoopbackHost } from "../gateway/net.js";
|
|
import { hasProxyEnvConfigured } from "../infra/net/proxy-env.js";
|
|
|
|
/** HTTP agent that never uses a proxy — for localhost CDP connections. */
|
|
const directHttpAgent = new http.Agent();
|
|
const directHttpsAgent = new https.Agent();
|
|
|
|
/**
|
|
* Returns a plain (non-proxy) agent for WebSocket or HTTP connections
|
|
* when the target is a loopback address. Returns `undefined` otherwise
|
|
* so callers fall through to their default behaviour.
|
|
*/
|
|
export function getDirectAgentForCdp(url: string): http.Agent | https.Agent | undefined {
|
|
try {
|
|
const parsed = new URL(url);
|
|
if (isLoopbackHost(parsed.hostname)) {
|
|
return parsed.protocol === "https:" || parsed.protocol === "wss:"
|
|
? directHttpsAgent
|
|
: directHttpAgent;
|
|
}
|
|
} catch {
|
|
// not a valid URL — let caller handle it
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Returns `true` when any proxy-related env var is set that could
|
|
* interfere with loopback connections.
|
|
*/
|
|
export function hasProxyEnv(): boolean {
|
|
return hasProxyEnvConfigured();
|
|
}
|
|
|
|
const LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
|
|
function noProxyAlreadyCoversLocalhost(): boolean {
|
|
const current = process.env.NO_PROXY || process.env.no_proxy || "";
|
|
return (
|
|
current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]")
|
|
);
|
|
}
|
|
|
|
export async function withNoProxyForLocalhost<T>(fn: () => Promise<T>): Promise<T> {
|
|
return await withNoProxyForCdpUrl("http://127.0.0.1", fn);
|
|
}
|
|
|
|
function isLoopbackCdpUrl(url: string): boolean {
|
|
try {
|
|
return isLoopbackHost(new URL(url).hostname);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
type NoProxySnapshot = {
|
|
noProxy: string | undefined;
|
|
noProxyLower: string | undefined;
|
|
applied: string;
|
|
};
|
|
|
|
class NoProxyLeaseManager {
|
|
private leaseCount = 0;
|
|
private snapshot: NoProxySnapshot | null = null;
|
|
|
|
acquire(url: string): (() => void) | null {
|
|
if (!isLoopbackCdpUrl(url) || !hasProxyEnv()) {
|
|
return null;
|
|
}
|
|
|
|
if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
|
|
const noProxy = process.env.NO_PROXY;
|
|
const noProxyLower = process.env.no_proxy;
|
|
const current = noProxy || noProxyLower || "";
|
|
const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
|
|
process.env.NO_PROXY = applied;
|
|
process.env.no_proxy = applied;
|
|
this.snapshot = { noProxy, noProxyLower, applied };
|
|
}
|
|
|
|
this.leaseCount += 1;
|
|
let released = false;
|
|
return () => {
|
|
if (released) {
|
|
return;
|
|
}
|
|
released = true;
|
|
this.release();
|
|
};
|
|
}
|
|
|
|
private release() {
|
|
if (this.leaseCount <= 0) {
|
|
return;
|
|
}
|
|
this.leaseCount -= 1;
|
|
if (this.leaseCount > 0 || !this.snapshot) {
|
|
return;
|
|
}
|
|
|
|
const { noProxy, noProxyLower, applied } = this.snapshot;
|
|
const currentNoProxy = process.env.NO_PROXY;
|
|
const currentNoProxyLower = process.env.no_proxy;
|
|
const untouched =
|
|
currentNoProxy === applied &&
|
|
(currentNoProxyLower === applied || currentNoProxyLower === undefined);
|
|
if (untouched) {
|
|
if (noProxy !== undefined) {
|
|
process.env.NO_PROXY = noProxy;
|
|
} else {
|
|
delete process.env.NO_PROXY;
|
|
}
|
|
if (noProxyLower !== undefined) {
|
|
process.env.no_proxy = noProxyLower;
|
|
} else {
|
|
delete process.env.no_proxy;
|
|
}
|
|
}
|
|
|
|
this.snapshot = null;
|
|
}
|
|
}
|
|
|
|
const noProxyLeaseManager = new NoProxyLeaseManager();
|
|
|
|
/**
|
|
* Scoped NO_PROXY bypass for loopback CDP URLs.
|
|
*
|
|
* This wrapper only mutates env vars for loopback destinations. On restore,
|
|
* it avoids clobbering external NO_PROXY changes that happened while calls
|
|
* were in-flight.
|
|
*/
|
|
export async function withNoProxyForCdpUrl<T>(url: string, fn: () => Promise<T>): Promise<T> {
|
|
const release = noProxyLeaseManager.acquire(url);
|
|
try {
|
|
return await fn();
|
|
} finally {
|
|
release?.();
|
|
}
|
|
}
|