UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

185 lines (184 loc) 7.77 kB
//#region src/infra/net/proxy-env.ts const PROXY_ENV_KEYS = [ "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy" ]; /** Return whether any supported proxy environment variable is non-blank. */ function hasProxyEnvConfigured(env = process.env) { for (const key of PROXY_ENV_KEYS) { const value = env[key]; if (typeof value === "string" && value.trim().length > 0) return true; } return false; } function normalizeProxyEnvValue(value) { if (typeof value !== "string") return; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } /** * Match undici EnvHttpProxyAgent semantics for env-based HTTP/S proxy selection: * - lower-case vars take precedence over upper-case * - HTTPS requests prefer https_proxy/HTTPS_PROXY, then fall back to http_proxy/HTTP_PROXY * - ALL_PROXY is ignored by EnvHttpProxyAgent */ function resolveEnvHttpProxyUrl(protocol, env = process.env) { const lowerHttpProxy = normalizeProxyEnvValue(env.http_proxy); const lowerHttpsProxy = normalizeProxyEnvValue(env.https_proxy); const httpProxy = lowerHttpProxy !== void 0 ? lowerHttpProxy : normalizeProxyEnvValue(env.HTTP_PROXY); const httpsProxy = lowerHttpsProxy !== void 0 ? lowerHttpsProxy : normalizeProxyEnvValue(env.HTTPS_PROXY); if (protocol === "https") return httpsProxy ?? httpProxy ?? void 0; return httpProxy ?? void 0; } /** Return whether EnvHttpProxyAgent-style HTTP/S proxy resolution finds a proxy URL. */ function hasEnvHttpProxyConfigured(protocol = "https", env = process.env) { return resolveEnvHttpProxyUrl(protocol, env) !== void 0; } function resolveEnvAllProxyUrl(env) { const lowerAllProxy = normalizeProxyEnvValue(env.all_proxy); return (lowerAllProxy !== void 0 ? lowerAllProxy : normalizeProxyEnvValue(env.ALL_PROXY)) ?? void 0; } /** * Build explicit options for undici's EnvHttpProxyAgent. * * EnvHttpProxyAgent does not read ALL_PROXY itself, but it accepts explicit * HTTP/HTTPS proxy overrides. Keep this helper separate from the * HTTP(S)-only URL helpers so SSRF trusted-env proxy gates do not widen. */ function resolveEnvHttpProxyAgentOptions(env = process.env) { const allProxy = resolveEnvAllProxyUrl(env); const httpProxy = resolveEnvHttpProxyUrl("http", env) ?? allProxy; const httpsProxy = resolveEnvHttpProxyUrl("https", env) ?? httpProxy; const options = { ...httpProxy ? { httpProxy } : {}, ...httpsProxy ? { httpsProxy } : {} }; return options.httpProxy || options.httpsProxy ? options : void 0; } /** Return whether explicit EnvHttpProxyAgent options can be built from the environment. */ function hasEnvHttpProxyAgentConfigured(env = process.env) { return resolveEnvHttpProxyAgentOptions(env) !== void 0; } /** Return whether a target URL should use configured HTTP/S env proxy variables. */ function shouldUseEnvHttpProxyForUrl(targetUrl, env = process.env) { let protocol; try { const parsed = new URL(targetUrl); if (parsed.protocol === "http:") protocol = "http"; else if (parsed.protocol === "https:") protocol = "https"; else return false; } catch { return false; } return hasEnvHttpProxyConfigured(protocol, env) && !matchesNoProxy(targetUrl, env); } /** * Check whether a target URL should bypass the HTTP proxy per NO_PROXY env var. * * Mirrors undici EnvHttpProxyAgent semantics * (`undici/lib/dispatcher/env-http-proxy-agent.js`): * - Entries separated by commas OR whitespace (undici splits on `/[,\s]/`) * - Case-insensitive * - Empty or missing → no bypass * - Bare `*` value → bypass everything * - Exact hostname match * - Leading-dot match (`.example.com` matches `foo.example.com`) * - Leading `*.` wildcard match (`*.example.com` matches `foo.example.com`); * undici normalizes via `.replace(/^\*?\./, '')`, so the bare domain also * matches (kept in sync with that behavior) * - Subdomain suffix match (`openai.com` matches `api.openai.com`) * - Optional `:port` suffix; when present, must match target port * - IPv6 literals in bracketed (`[::1]`) or bare (`::1`) form * - OpenClaw extension: IPv4 CIDR and octet-wildcard entries * (`100.64.0.0/10`, `100.64.*`) bypass the trusted env proxy mode before * undici's EnvHttpProxyAgent is selected. * * Undici does not export its matcher, so this is a targeted reimplementation * kept in sync with the upstream file above. Paired with * `hasEnvHttpProxyConfigured` this gates the trusted-env-proxy auto-upgrade * in provider HTTP helpers; see openclaw#64974 review thread on NO_PROXY * SSRF bypass. */ function matchesNoProxy(targetUrl, env = process.env) { const raw = normalizeProxyEnvValue(env.no_proxy) ?? normalizeProxyEnvValue(env.NO_PROXY); if (!raw) return false; let parsed; try { parsed = new URL(targetUrl); } catch { return false; } const targetHost = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, ""); if (!targetHost) return false; if (raw === "*") return true; const targetPort = parsed.port !== "" ? parsed.port : parsed.protocol === "https:" ? "443" : parsed.protocol === "http:" ? "80" : ""; for (const rawEntry of raw.split(/[,\s]/)) { const entry = rawEntry.trim().toLowerCase(); if (!entry) continue; let entryHost; let entryPort; if (entry.startsWith("[")) { const m = entry.match(/^\[([^\]]+)\](?::(\d+))?$/); if (!m) continue; entryHost = m[1]; entryPort = m[2]; } else { const firstColonIdx = entry.indexOf(":"); const lastColonIdx = entry.lastIndexOf(":"); if (firstColonIdx > -1 && firstColonIdx === lastColonIdx && /^\d+$/.test(entry.slice(lastColonIdx + 1))) { entryHost = entry.slice(0, lastColonIdx); entryPort = entry.slice(lastColonIdx + 1); } else entryHost = entry; } if (entryPort && entryPort !== targetPort) continue; const normalizedEntry = entryHost.replace(/^\*\./, "").replace(/^\./, ""); if (!normalizedEntry || normalizedEntry === "*") continue; if (matchesIpv4NoProxyPattern(targetHost, normalizedEntry)) return true; if (targetHost === normalizedEntry) return true; if (targetHost.endsWith("." + normalizedEntry)) return true; } return false; } function parseIpv4Address(host) { const parts = host.split("."); if (parts.length !== 4) return; let value = 0; for (const part of parts) { if (!/^\d{1,3}$/.test(part)) return; const octet = Number(part); if (!Number.isInteger(octet) || octet < 0 || octet > 255) return; value = value << 8 | octet; } return value >>> 0; } function matchesIpv4NoProxyPattern(targetHost, entryHost) { const target = parseIpv4Address(targetHost); if (target === void 0) return false; const cidrMatch = entryHost.match(/^(\d{1,3}(?:\.\d{1,3}){3})\/(\d{1,2})$/); if (cidrMatch) { const network = parseIpv4Address(cidrMatch[1]); const prefixLength = Number(cidrMatch[2]); if (network === void 0 || prefixLength < 0 || prefixLength > 32) return false; const mask = prefixLength === 0 ? 0 : 4294967295 << 32 - prefixLength >>> 0; return (target & mask) === (network & mask); } if (!entryHost.includes("*")) return false; const targetParts = targetHost.split("."); const patternParts = entryHost.split("."); if (patternParts.length > 4 || patternParts.length === 0) return false; for (let index = 0; index < patternParts.length; index += 1) { const part = patternParts[index]; if (part === "*") { if (index === patternParts.length - 1) return true; continue; } if (!/^\d{1,3}$/.test(part) || Number(part) !== Number(targetParts[index])) return false; } return patternParts.length === targetParts.length; } //#endregion export { matchesNoProxy as a, shouldUseEnvHttpProxyForUrl as c, hasProxyEnvConfigured as i, hasEnvHttpProxyAgentConfigured as n, resolveEnvHttpProxyAgentOptions as o, hasEnvHttpProxyConfigured as r, resolveEnvHttpProxyUrl as s, PROXY_ENV_KEYS as t };