fix(proxy): auto-fallback to insecure TLS on self-signed cert errors

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
decoluaandClaude Code committed 2026-09-28 18:51:31 +07:00
1 parent aafe300226
commit b58bd80406
1 file changed
+48 -12
+48 -12
View File
@@ -99,6 +99,20 @@ async function tryGotScrapingFetch(url, options) {
// DNS cache — use Map to avoid prototype pollution via malformed hostnames // DNS cache — use Map to avoid prototype pollution via malformed hostnames
const DNS_CACHE = new Map(); const DNS_CACHE = new Map();
const TLS_CERT_ERRORS = new Set([
"SELF_SIGNED_CERT_IN_CHAIN",
"DEPTH_ZERO_SELF_SIGNED_CERT",
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
"UNABLE_TO_GET_ISSUER_CERT",
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
"CERT_HAS_EXPIRED",
"ERR_TLS_CERT_ALTNAME_INVALID",
]);
function isTlsCertError(err) {
const code = err?.cause?.code || err?.code;
return TLS_CERT_ERRORS.has(code);
}
const MITM_BYPASS_HOSTS = [ const MITM_BYPASS_HOSTS = [
"cloudcode-pa.googleapis.com", "cloudcode-pa.googleapis.com",
"daily-cloudcode-pa.googleapis.com", "daily-cloudcode-pa.googleapis.com",
@@ -216,20 +230,44 @@ function resolveConnectionProxyUrl(targetUrl, proxyOptions) {
/** /**
* Create proxy dispatcher lazily (undici-compatible) * Create proxy dispatcher lazily (undici-compatible)
*/ */
async function getDispatcher(proxyUrl) { async function getDispatcher(proxyUrl, insecure = false) {
const normalized = normalizeProxyUrl(proxyUrl); const normalized = normalizeProxyUrl(proxyUrl);
if (!normalized) return null; if (!normalized && !insecure) return null;
if (!proxyDispatchers.has(normalized)) { const key = `${normalized || "direct"}::${insecure ? "insecure" : "secure"}`;
if (!proxyDispatchers.has(key)) {
// Evict oldest entry if max size reached // Evict oldest entry if max size reached
if (proxyDispatchers.size >= MEMORY_CONFIG.proxyDispatchersMaxSize) { if (proxyDispatchers.size >= MEMORY_CONFIG.proxyDispatchersMaxSize) {
proxyDispatchers.delete(proxyDispatchers.keys().next().value); proxyDispatchers.delete(proxyDispatchers.keys().next().value);
} }
const { ProxyAgent } = await import("undici"); const { Agent, ProxyAgent } = await import("undici");
proxyDispatchers.set(normalized, new ProxyAgent({ uri: normalized })); const connect = insecure ? { rejectUnauthorized: false } : undefined;
const dispatcher = normalized
? new ProxyAgent({ uri: normalized, ...(insecure ? { requestTls: connect } : {}) })
: new Agent({ connect });
proxyDispatchers.set(key, dispatcher);
} }
return proxyDispatchers.get(normalized); return proxyDispatchers.get(key);
}
async function fetchWithTlsFallback(url, options, proxyUrl) {
try {
const dispatcher = proxyUrl ? await getDispatcher(proxyUrl) : undefined;
return await originalFetch(url, dispatcher ? { ...options, dispatcher } : options);
} catch (err) {
const isStrictSsl = process.env.STRICT_SSL === "true" || process.env.STRICT_SSL === "1";
if (!isStrictSsl && isTlsCertError(err)) {
if (options.body && typeof options.body.getReader === "function" && options.body.locked) {
throw err;
}
// ponytail: in-memory insecure agent fallback for self-signed MITM corporate/antivirus certs
console.warn(`[ProxyFetch] TLS cert verification failed (${err.cause?.code || err.code}), retrying with insecure TLS: ${url}`);
const insecureDispatcher = await getDispatcher(proxyUrl, true);
return await originalFetch(url, { ...options, dispatcher: insecureDispatcher });
}
throw err;
}
} }
/** /**
@@ -318,8 +356,7 @@ export async function proxyAwareFetch(url, options = {}, proxyOptions = null) {
if (proxyUrl) { if (proxyUrl) {
// Proxy resolves DNS externally (not affected by /etc/hosts) — use proxy directly // Proxy resolves DNS externally (not affected by /etc/hosts) — use proxy directly
try { try {
const dispatcher = await getDispatcher(proxyUrl); return await fetchWithTlsFallback(url, options, proxyUrl);
return await originalFetch(url, { ...options, dispatcher });
} catch (proxyError) { } catch (proxyError) {
if (proxyOptions?.strictProxy === true) { if (proxyOptions?.strictProxy === true) {
throw new Error(`[ProxyFetch] Proxy required but failed (strictProxy=true): ${proxyError.message}`); throw new Error(`[ProxyFetch] Proxy required but failed (strictProxy=true): ${proxyError.message}`);
@@ -339,15 +376,14 @@ export async function proxyAwareFetch(url, options = {}, proxyOptions = null) {
if (proxyUrl) { if (proxyUrl) {
try { try {
const dispatcher = await getDispatcher(proxyUrl); return await fetchWithTlsFallback(url, options, proxyUrl);
return await originalFetch(url, { ...options, dispatcher });
} catch (proxyError) { } catch (proxyError) {
// If strictProxy is enabled, fail hard instead of falling back to direct // If strictProxy is enabled, fail hard instead of falling back to direct
if (proxyOptions?.strictProxy === true) { if (proxyOptions?.strictProxy === true) {
throw new Error(`[ProxyFetch] Proxy required but failed (strictProxy=true): ${proxyError.message}`); throw new Error(`[ProxyFetch] Proxy required but failed (strictProxy=true): ${proxyError.message}`);
} }
console.warn(`[ProxyFetch] Proxy failed, falling back to direct: ${proxyError.message}`); console.warn(`[ProxyFetch] Proxy failed, falling back to direct: ${proxyError.message}`);
return originalFetch(url, options); return fetchWithTlsFallback(url, options, null);
} }
} }
@@ -371,7 +407,7 @@ export async function proxyAwareFetch(url, options = {}, proxyOptions = null) {
// got-scraping disabled — use native fetch directly // got-scraping disabled — use native fetch directly
// (Re-enable per-host by wrapping with tryGotScrapingFetch when needed) // (Re-enable per-host by wrapping with tryGotScrapingFetch when needed)
return originalFetch(url, options); return fetchWithTlsFallback(url, options, null);
} }
/** /**