mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-10 01:28:25 +00:00
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
This commit is contained in:
parent
0f54b7d7a3
commit
5f7345fdc8
2 changed files with 47 additions and 78 deletions
|
|
@ -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<string | undefined> {
|
||||
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<string | undefined> {
|
||||
|
|
@ -49,9 +57,7 @@ async function systemProxyUrlForRequest(url: URL): Promise<string | undefined> {
|
|||
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<SystemProxyCache> {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue