| import { Readable } from "stream"; |
| import { MEMORY_CONFIG } from "../config/runtimeConfig.js"; |
| import { dbg } from "./debugLog.js"; |
|
|
| const originalFetch = globalThis.fetch; |
| const proxyDispatchers = new Map(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| const DNS_CACHE = new Map(); |
| const MITM_BYPASS_HOSTS = [ |
| "cloudcode-pa.googleapis.com", |
| "daily-cloudcode-pa.googleapis.com", |
| "api.individual.githubcopilot.com", |
| "q.us-east-1.amazonaws.com", |
| "codewhisperer.us-east-1.amazonaws.com", |
| "api2.cursor.sh", |
| ]; |
| const GOOGLE_DNS_SERVERS = ["8.8.8.8", "8.8.4.4"]; |
| const HTTPS_PORT = 443; |
| const HTTP_SUCCESS_MIN = 200; |
| const HTTP_SUCCESS_MAX = 300; |
|
|
| function normalizeString(value) { |
| if (value === undefined || value === null) return ""; |
| return String(value).trim(); |
| } |
|
|
| |
| |
| |
| async function resolveRealIP(hostname) { |
| const cached = DNS_CACHE.get(hostname); |
| if (cached && Date.now() < cached.expiry) return cached.ip; |
|
|
| try { |
| const dns = await import("dns"); |
| const { promisify } = await import("util"); |
| const resolver = new dns.Resolver(); |
| resolver.setServers(GOOGLE_DNS_SERVERS); |
| const resolve4 = promisify(resolver.resolve4.bind(resolver)); |
| const addresses = await resolve4(hostname); |
| DNS_CACHE.set(hostname, { ip: addresses[0], expiry: Date.now() + MEMORY_CONFIG.dnsCacheTtlMs }); |
| return addresses[0]; |
| } catch (error) { |
| console.warn(`[ProxyFetch] DNS resolve failed for ${hostname}:`, error.message); |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| function shouldBypassMitmDns(url) { |
| try { |
| const hostname = new URL(url).hostname; |
| return MITM_BYPASS_HOSTS.some(host => hostname.includes(host)); |
| } catch { return false; } |
| } |
|
|
| function shouldBypassByNoProxy(targetUrl, noProxyValue) { |
| const noProxy = normalizeString(noProxyValue); |
| if (!noProxy) return false; |
|
|
| let hostname; |
| try { hostname = new URL(targetUrl).hostname.toLowerCase(); } catch { return false; } |
| const patterns = noProxy.split(",").map((p) => p.trim().toLowerCase()).filter(Boolean); |
|
|
| return patterns.some((pattern) => { |
| if (pattern === "*") return true; |
| if (pattern.startsWith(".")) return hostname.endsWith(pattern) || hostname === pattern.slice(1); |
| return hostname === pattern || hostname.endsWith(`.${pattern}`); |
| }); |
| } |
|
|
| |
| |
| |
| function getEnvProxyUrl(targetUrl) { |
| const noProxy = process.env.NO_PROXY || process.env.no_proxy; |
| if (shouldBypassByNoProxy(targetUrl, noProxy)) return null; |
|
|
| let protocol; |
| try { protocol = new URL(targetUrl).protocol; } catch { return null; } |
|
|
| if (protocol === "https:") { |
| return process.env.HTTPS_PROXY || process.env.https_proxy || |
| process.env.ALL_PROXY || process.env.all_proxy; |
| } |
|
|
| return process.env.HTTP_PROXY || process.env.http_proxy || |
| process.env.ALL_PROXY || process.env.all_proxy; |
| } |
|
|
| |
| |
| |
| function normalizeProxyUrl(proxyUrl) { |
| const normalizedInput = normalizeString(proxyUrl); |
| if (!normalizedInput) return null; |
|
|
| try { |
|
|
| new URL(normalizedInput); |
| return normalizedInput; |
| } catch { |
| |
| return `http://${normalizedInput}`; |
| } |
| } |
|
|
| function resolveConnectionProxyUrl(targetUrl, proxyOptions) { |
| const enabled = proxyOptions?.enabled === true || proxyOptions?.connectionProxyEnabled === true; |
| if (!enabled) return null; |
|
|
| const proxyUrlRaw = normalizeString(proxyOptions?.url ?? proxyOptions?.connectionProxyUrl); |
| if (!proxyUrlRaw) return null; |
|
|
| const noProxy = normalizeString(proxyOptions?.noProxy ?? proxyOptions?.connectionNoProxy); |
| if (noProxy && shouldBypassByNoProxy(targetUrl, noProxy)) return null; |
|
|
| return normalizeProxyUrl(proxyUrlRaw); |
| } |
|
|
| |
| |
| |
| async function getDispatcher(proxyUrl) { |
| const normalized = normalizeProxyUrl(proxyUrl); |
| if (!normalized) return null; |
|
|
| if (!proxyDispatchers.has(normalized)) { |
| |
| if (proxyDispatchers.size >= MEMORY_CONFIG.proxyDispatchersMaxSize) { |
| proxyDispatchers.delete(proxyDispatchers.keys().next().value); |
| } |
| const { ProxyAgent } = await import("undici"); |
| proxyDispatchers.set(normalized, new ProxyAgent({ uri: normalized })); |
| } |
|
|
| return proxyDispatchers.get(normalized); |
| } |
|
|
| |
| |
| |
| async function createBypassRequest(parsedUrl, realIP, options) { |
| const httpsModule = await import("https"); |
| const netModule = await import("net"); |
| |
| const https = httpsModule.default ?? httpsModule; |
| const net = netModule.default ?? netModule; |
|
|
| return new Promise((resolve, reject) => { |
| const socket = new net.Socket(); |
|
|
| socket.connect(HTTPS_PORT, realIP, () => { |
| const reqOptions = { |
| socket, |
| |
| |
| |
| |
| |
| |
| servername: parsedUrl.hostname, |
| path: parsedUrl.pathname + parsedUrl.search, |
| method: options.method || "POST", |
| headers: { |
| ...options.headers, |
| Host: parsedUrl.hostname, |
| }, |
| }; |
|
|
| const req = https.request(reqOptions, (res) => { |
| const response = { |
| ok: res.statusCode >= HTTP_SUCCESS_MIN && res.statusCode < HTTP_SUCCESS_MAX, |
| status: res.statusCode, |
| statusText: res.statusMessage, |
| headers: new Map(Object.entries(res.headers)), |
| body: Readable.toWeb(res), |
| text: async () => { |
| const chunks = []; |
| for await (const chunk of res) chunks.push(chunk); |
| return Buffer.concat(chunks).toString(); |
| }, |
| json: async () => JSON.parse(await response.text()), |
| }; |
| resolve(response); |
| }); |
|
|
| req.on("error", reject); |
| if (options.body) { |
| req.write(typeof options.body === "string" ? options.body : JSON.stringify(options.body)); |
| } |
| req.end(); |
| }); |
|
|
| socket.on("error", reject); |
| }); |
| } |
|
|
| export async function proxyAwareFetch(url, options = {}, proxyOptions = null) { |
| const targetUrl = typeof url === "string" ? url : url.toString(); |
|
|
| |
| const vercelRelayUrl = normalizeString(proxyOptions?.vercelRelayUrl); |
| if (vercelRelayUrl) { |
| const parsed = new URL(targetUrl); |
| const relayHeaders = { |
| ...options.headers, |
| "x-relay-target": `${parsed.protocol}//${parsed.host}`, |
| "x-relay-path": `${parsed.pathname}${parsed.search}`, |
| }; |
| return originalFetch(vercelRelayUrl, { ...options, headers: relayHeaders }); |
| } |
|
|
| const connectionProxyUrl = resolveConnectionProxyUrl(targetUrl, proxyOptions); |
| const envProxyUrl = connectionProxyUrl ? null : normalizeProxyUrl(getEnvProxyUrl(targetUrl)); |
| const proxyUrl = connectionProxyUrl || envProxyUrl; |
|
|
| |
| if (shouldBypassMitmDns(targetUrl)) { |
| if (proxyUrl) { |
| |
| try { |
| const dispatcher = await getDispatcher(proxyUrl); |
| return await originalFetch(url, { ...options, dispatcher }); |
| } catch (proxyError) { |
| 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 bypass: ${proxyError.message}`); |
| } |
| } |
| |
| try { |
| const parsedUrl = new URL(targetUrl); |
| const realIP = await resolveRealIP(parsedUrl.hostname); |
| if (realIP) return await createBypassRequest(parsedUrl, realIP, options); |
| } catch (error) { |
| console.warn(`[ProxyFetch] MITM bypass failed: ${error.message}`); |
| } |
| } |
|
|
| if (proxyUrl) { |
| try { |
| const dispatcher = await getDispatcher(proxyUrl); |
| return await originalFetch(url, { ...options, dispatcher }); |
| } catch (proxyError) { |
| |
| 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 originalFetch(url, options); |
| } |
|
|
| |
| |
| |
| async function patchedFetch(url, options = {}) { |
| return proxyAwareFetch(url, options, null); |
| } |
|
|
| |
| if (globalThis.fetch !== patchedFetch) { |
| globalThis.fetch = patchedFetch; |
| } |
|
|
| export default patchedFetch; |
|
|