From b58bd80406a24030acac6312fce519534c61947a Mon Sep 17 00:00:00 2001 From: decolua Date: Mon, 28 Sep 2026 18:51:31 +0700 Subject: [PATCH] fix(proxy): auto-fallback to insecure TLS on self-signed cert errors Co-Authored-By: Claude Code --- open-sse/utils/proxyFetch.js | 60 ++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/open-sse/utils/proxyFetch.js b/open-sse/utils/proxyFetch.js index d7705c77..af819cc5 100644 --- a/open-sse/utils/proxyFetch.js +++ b/open-sse/utils/proxyFetch.js @@ -99,6 +99,20 @@ async function tryGotScrapingFetch(url, options) { // DNS cache — use Map to avoid prototype pollution via malformed hostnames 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 = [ "cloudcode-pa.googleapis.com", "daily-cloudcode-pa.googleapis.com", @@ -216,20 +230,44 @@ function resolveConnectionProxyUrl(targetUrl, proxyOptions) { /** * Create proxy dispatcher lazily (undici-compatible) */ -async function getDispatcher(proxyUrl) { +async function getDispatcher(proxyUrl, insecure = false) { 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 if (proxyDispatchers.size >= MEMORY_CONFIG.proxyDispatchersMaxSize) { proxyDispatchers.delete(proxyDispatchers.keys().next().value); } - const { ProxyAgent } = await import("undici"); - proxyDispatchers.set(normalized, new ProxyAgent({ uri: normalized })); + const { Agent, ProxyAgent } = await import("undici"); + 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) { // Proxy resolves DNS externally (not affected by /etc/hosts) — use proxy directly try { - const dispatcher = await getDispatcher(proxyUrl); - return await originalFetch(url, { ...options, dispatcher }); + return await fetchWithTlsFallback(url, options, proxyUrl); } catch (proxyError) { if (proxyOptions?.strictProxy === true) { 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) { try { - const dispatcher = await getDispatcher(proxyUrl); - return await originalFetch(url, { ...options, dispatcher }); + return await fetchWithTlsFallback(url, options, proxyUrl); } catch (proxyError) { // If strictProxy is enabled, fail hard instead of falling back to direct if (proxyOptions?.strictProxy === true) { throw new Error(`[ProxyFetch] Proxy required but failed (strictProxy=true): ${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 // (Re-enable per-host by wrapping with tryGotScrapingFetch when needed) - return originalFetch(url, options); + return fetchWithTlsFallback(url, options, null); } /**