From 5f7345fdc8f5ed4cfcec32d5d0696ce9fa6cdb16 Mon Sep 17 00:00:00 2001 From: 810senpai114514 <810senpai114514@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:20:26 +0800 Subject: [PATCH] fix(gateway): proxy passthrough via globalThis.fetch and fix NO_PROXY parsing Replace setGlobalDispatcher with globalThis.fetch wrapper to work around bundled undici instance isolation in ai-gateway subprocess. Also: - Extract shared readEnvProxyUrl() to eliminate duplicated env proxy logic - Fix IPv6 loopback detection (URL.hostname returns [::1] with brackets) - Fix URL object handling in preload bypass check - Fix *.domain wildcard and host:port NO_PROXY matching - Fix inverted fail-safe: bypass on parse error instead of proxying - Add writable guard for globalThis.fetch assignment --- src/main/system-proxy-fetch.ts | 14 +++-- src/server/gateway/service.ts | 111 +++++++++++---------------------- 2 files changed, 47 insertions(+), 78 deletions(-) diff --git a/src/main/system-proxy-fetch.ts b/src/main/system-proxy-fetch.ts index dfb7538..fd6d941 100644 --- a/src/main/system-proxy-fetch.ts +++ b/src/main/system-proxy-fetch.ts @@ -38,10 +38,18 @@ export async function fetchWithSystemProxy(input: RequestInfo | URL, init?: Requ } as FetchInitWithDispatcher); } +export function readEnvProxyUrl(): string | undefined { + const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy || + process.env.HTTP_PROXY || process.env.http_proxy; + return envProxy ? envProxy.trim() : undefined; +} + export async function getSystemProxyUrlForProtocol(protocol: "http" | "https" = "https"): Promise { const cache = await readSystemProxy(); const server = proxyServerForRequest(cache.upstreamProxy, protocol); - return server ? formatProxyUrl(server) : undefined; + if (server) return formatProxyUrl(server); + + return readEnvProxyUrl(); } async function systemProxyUrlForRequest(url: URL): Promise { @@ -49,9 +57,7 @@ async function systemProxyUrlForRequest(url: URL): Promise { const server = proxyServerForRequest(cache.upstreamProxy, url.protocol === "https:" ? "https" : "http"); if (server) return formatProxyUrl(server); - const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy || - process.env.HTTP_PROXY || process.env.http_proxy; - return envProxy ? envProxy.trim() : undefined; + return readEnvProxyUrl(); } async function readSystemProxy(): Promise { diff --git a/src/server/gateway/service.ts b/src/server/gateway/service.ts index 56b9e1b..9a3323a 100644 --- a/src/server/gateway/service.ts +++ b/src/server/gateway/service.ts @@ -6054,82 +6054,45 @@ function writeGatewayProxyPreloadFile(config: AppConfig, upstreamProxyUrl: strin file, [ "\"use strict\";", - "const proxyUrl = process.env.CCR_UPSTREAM_PROXY_URL;", - "if (proxyUrl) {", - " const undiciModule = process.env.CCR_UNDICI_MODULE || \"undici\";", - " const { Agent, ProxyAgent, setGlobalDispatcher } = require(undiciModule);", - " const directAgent = new Agent();", - " const proxyAgent = new ProxyAgent(proxyUrl);", - " const noProxyEntries = String(process.env.NO_PROXY || process.env.no_proxy || \"\")", - " .split(\",\")", - " .map((entry) => entry.trim())", - " .filter(Boolean);", - " function normalizeHost(host) {", - " return String(host || \"\").replace(/^\\[/, \"\").replace(/\\]$/, \"\").replace(/\\.$/, \"\").toLowerCase();", - " }", - " function isLoopbackHost(host) {", - " return host === \"localhost\" || host === \"::1\" || host === \"0:0:0:0:0:0:0:1\" || host === \"127.0.0.1\" || host.startsWith(\"127.\");", - " }", - " function splitNoProxyEntry(entry) {", - " let value = String(entry || \"\").trim().toLowerCase();", - " if (!value || value === \"*\") {", - " return { host: value, port: \"\" };", - " }", - " if (value.startsWith(\"[\")) {", - " const end = value.indexOf(\"]\");", - " if (end >= 0) {", - " const remainder = value.slice(end + 1);", - " return { host: normalizeHost(value.slice(1, end)), port: remainder.startsWith(\":\") ? remainder.slice(1) : \"\" };", - " }", - " }", - " const colon = value.lastIndexOf(\":\");", - " if (colon > 0 && value.indexOf(\":\") === colon) {", - " return { host: normalizeHost(value.slice(0, colon)), port: value.slice(colon + 1) };", - " }", - " return { host: normalizeHost(value), port: \"\" };", - " }", - " function noProxyMatches(host, port, entry) {", - " const parsed = splitNoProxyEntry(entry);", - " if (parsed.host === \"*\") {", - " return true;", - " }", - " if (!parsed.host || (parsed.port && parsed.port !== port)) {", - " return false;", - " }", - " if (parsed.host.startsWith(\"*.\")) {", - " const suffix = parsed.host.slice(1);", - " return host.endsWith(suffix);", - " }", - " if (parsed.host.startsWith(\".\")) {", - " return host === parsed.host.slice(1) || host.endsWith(parsed.host);", - " }", - " return host === parsed.host || host.endsWith(`.${parsed.host}`);", - " }", - " function shouldBypassProxy(origin) {", + "const up = process.env.CCR_UPSTREAM_PROXY_URL;", + "const um = process.env.CCR_UNDICI_MODULE;", + "if (up && um) {", + " const { ProxyAgent } = require(um);", + " const agent = new ProxyAgent(up);", + " const realFetch = globalThis.fetch.bind(globalThis);", + " const raw = (process.env.NO_PROXY || process.env.no_proxy || '').toLowerCase();", + " const byp = raw.split(',').map((s) => s.trim()).filter(Boolean);", + " const norm = (h) => h.replace(/^\\[/, '').replace(/\\]$/, '').replace(/\\.$/, '');", + " const isLP = (h) => h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '0:0:0:0:0:0:0:1' || h === '0.0.0.0' || h.startsWith('127.');", + " const shouldBypass = (input) => {", + " let h;", " try {", - " const url = new URL(String(origin));", - " const host = normalizeHost(url.hostname);", - " const port = url.port || (url.protocol === \"https:\" ? \"443\" : url.protocol === \"http:\" ? \"80\" : \"\");", - " return isLoopbackHost(host) || noProxyEntries.some((entry) => noProxyMatches(host, port, entry));", - " } catch {", - " return false;", - " }", + " const u = typeof input === 'string' ? new URL(input) : input instanceof URL ? input : new URL(input && input.url ? input.url : String(input));", + " h = norm(u.hostname);", + " } catch { return true; }", + " if (!h) return false;", + " if (isLP(h)) return true;", + " return byp.some((p) => {", + " if (p === '*') return true;", + " const s = p.split(':');", + " const ph = norm(s[0]);", + " if (s.length === 2 && s[1]) {", + " if (h !== ph) return false;", + " try { return new URL(input).port === s[1]; } catch { return false; }", + " }", + " if (ph.startsWith('*.')) return h.endsWith(ph.slice(1));", + " if (ph.startsWith('.')) return h.endsWith(ph) || h === ph.slice(1);", + " return h === ph;", + " });", + " };", + " const patched = function(input, init) {", + " if (init && init.dispatcher) return realFetch(input, init);", + " if (shouldBypass(input)) return realFetch(input, init);", + " return realFetch(input, Object.assign({}, init, { dispatcher: agent }));", + " };", + " if (Object.getOwnPropertyDescriptor(globalThis, 'fetch')?.writable) {", + " globalThis.fetch = patched;", " }", - " setGlobalDispatcher({", - " dispatch(options, handler) {", - " return shouldBypassProxy(options && options.origin)", - " ? directAgent.dispatch(options, handler)", - " : proxyAgent.dispatch(options, handler);", - " },", - " close(callback) {", - " Promise.all([directAgent.close(), proxyAgent.close()]).then(() => callback && callback(), callback);", - " },", - " destroy(error, callback) {", - " directAgent.destroy(error);", - " proxyAgent.destroy(error);", - " if (callback) callback();", - " }", - " });", "}" ].join("\n"), "utf8"