openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
208 lines (207 loc) • 6.83 kB
JavaScript
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) ?? "",
};
}