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:
1 parent
aafe300226
commit
b58bd80406
1 file changed
+48
-12
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user