UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

208 lines (207 loc) • 6.83 kB
import { formatUrl, redactProxyUrl } from "./shared.js"; export const EMPTY_PROXY_ENV = { HTTP_PROXY: undefined, HTTPS_PROXY: undefined, ALL_PROXY: undefined, NO_PROXY: undefined, http_proxy: undefined, https_proxy: undefined, all_proxy: undefined, no_proxy: undefined, }; export function readProxyEnv() { return { HTTP_PROXY: process.env.HTTP_PROXY, HTTPS_PROXY: process.env.HTTPS_PROXY, ALL_PROXY: process.env.ALL_PROXY, NO_PROXY: process.env.NO_PROXY, http_proxy: process.env.http_proxy, https_proxy: process.env.https_proxy, all_proxy: process.env.all_proxy, no_proxy: process.env.no_proxy, }; } function normalizeEnvValue(value) { const trimmed = value?.trim(); return trimmed ? trimmed : undefined; } function upperProxyEnvKey(key) { switch (key) { case "http_proxy": return "HTTP_PROXY"; case "https_proxy": return "HTTPS_PROXY"; case "all_proxy": return "ALL_PROXY"; case "no_proxy": return "NO_PROXY"; } } export function readProxyEnvValue(env, key) { return normalizeEnvValue(env[key]) ?? normalizeEnvValue(env[upperProxyEnvKey(key)]); } export function proxyUrlWithDefaultScheme(proxyUrl) { return proxyUrl.includes("://") ? proxyUrl : `http://${proxyUrl}`; } function normalizeAmbientProxyUrl(proxyUrl) { if (proxyUrl === undefined) { return undefined; } try { const url = new URL(proxyUrlWithDefaultScheme(proxyUrl)); return url.protocol === "http:" || url.protocol === "https:" ? url.href : undefined; } catch { return undefined; } } function defaultPort(protocol) { if (protocol === "http:" || protocol === "ws:") { return 80; } if (protocol === "https:" || protocol === "wss:") { return 443; } return 0; } function matchesNoProxy(url, env) { const rawNoProxy = readProxyEnvValue(env, "no_proxy")?.toLowerCase(); if (!rawNoProxy) { return false; } if (rawNoProxy === "*") { return true; } const hostname = normalizeNoProxyHost(url.hostname); const port = Number.parseInt(url.port, 10) || defaultPort(url.protocol); for (const rawEntry of rawNoProxy.split(/[,\s]/)) { if (!rawEntry) { continue; } const { host: parsedHost, port: entryPort } = parseNoProxyEntry(rawEntry); let entryHost = normalizeNoProxyHost(parsedHost); if (entryPort && entryPort !== port) { continue; } if (!/^[.*]/.test(entryHost)) { if (hostname === entryHost) { return true; } continue; } if (entryHost.startsWith("*")) { entryHost = entryHost.slice(1); } if (entryHost.startsWith(".") && (hostname === entryHost.slice(1) || hostname.endsWith(entryHost))) { return true; } if (!entryHost.startsWith(".") && hostname.endsWith(entryHost)) { return true; } } return false; } function normalizeNoProxyHost(hostname) { const value = hostname.trim().toLowerCase(); let end = value.length; while (end > 0 && value[end - 1] === ".") { end -= 1; } const normalized = value.slice(0, end); return normalized.startsWith("[") && normalized.endsWith("]") ? normalized.slice(1, -1) : normalized; } function parseNoProxyEntry(entry) { const bracketedIpv6 = entry.match(/^\[([^\]]+)\](?::(\d+))?$/); if (bracketedIpv6) { return { host: bracketedIpv6[1] ?? "", port: bracketedIpv6[2] ? Number.parseInt(bracketedIpv6[2], 10) : 0, }; } const lastColon = entry.lastIndexOf(":"); const hasSingleColon = lastColon !== -1 && entry.indexOf(":") === lastColon; if (hasSingleColon) { const possiblePort = entry.slice(lastColon + 1); if (/^\d+$/.test(possiblePort)) { return { host: entry.slice(0, lastColon), port: Number.parseInt(possiblePort, 10), }; } } return { host: entry, port: 0 }; } function proxyEnvKeyForProtocol(protocol) { if (protocol === "http:" || protocol === "ws:") { return "http_proxy"; } if (protocol === "https:" || protocol === "wss:") { return "https_proxy"; } return undefined; } function supportsProxyForUrlProtocol(protocol) { return protocol === "http:" || protocol === "https:" || protocol === "ws:" || protocol === "wss:"; } function resolveAmbientProxyEnvValue(env, key) { return normalizeAmbientProxyUrl(readProxyEnvValue(env, key)); } export function resolveAmbientProxyForUrl(url, env) { let parsedUrl; try { parsedUrl = url instanceof URL ? new URL(url.href) : new URL(url); } catch { return undefined; } const protocol = parsedUrl.protocol; if (!supportsProxyForUrlProtocol(protocol)) { return undefined; } if (matchesNoProxy(parsedUrl, env)) { return undefined; } const protocolProxyKey = proxyEnvKeyForProtocol(protocol); if (protocolProxyKey === undefined) { return undefined; } return (resolveAmbientProxyEnvValue(env, protocolProxyKey) ?? resolveAmbientProxyEnvValue(env, "all_proxy")); } export function createAmbientProxyResolver(env) { const configuredProxy = resolveAmbientProxyEnvValue(env, "http_proxy") ?? resolveAmbientProxyEnvValue(env, "https_proxy") ?? resolveAmbientProxyEnvValue(env, "all_proxy"); return { active: configuredProxy !== undefined, describeProxy: () => configuredProxy ? redactProxyUrl(proxyUrlWithDefaultScheme(configuredProxy)) : undefined, explain: (url, surface) => { const formattedUrl = formatUrl(url); const parsedUrl = new URL(formattedUrl); const proxyUrl = resolveAmbientProxyForUrl(formattedUrl, env); if (proxyUrl !== undefined) { return { kind: "proxied", reason: "ambient-proxy-active", surface, url: formattedUrl, proxyUrl: redactProxyUrl(proxyUrl), }; } return { kind: "direct", reason: supportsProxyForUrlProtocol(parsedUrl.protocol) && matchesNoProxy(parsedUrl, env) ? "no-proxy-match" : "ambient-proxy-not-configured", surface, url: formattedUrl, }; }, getProxyForUrl: (url) => resolveAmbientProxyForUrl(url, env) ?? "", }; }