Files
OmniRoute/open-sse/utils/proxyFetch.ts
diegosouzapw 6e9b23c8e2 fix(providers): support batch testing for web, search, and audio
Add dedicated batch test modes for web-cookie, search, and audio
providers in the dashboard, API route, and request validation so
category-level testing targets the correct connections.

Rename legacy qoder refresh and usage helpers from iflow to qoder
for consistency, and tighten regex handling in response cleaning,
thinking compression, and proxy matching to address edge cases and
static analysis findings.

Also update related tests, typing fixes, and README star history
embeds.
2026-04-16 11:52:53 -03:00

294 lines
9.4 KiB
TypeScript

import { AsyncLocalStorage } from "node:async_hooks";
import { fetch as undiciFetch } from "undici";
import {
createProxyDispatcher,
getDefaultDispatcher,
normalizeProxyUrl,
proxyConfigToUrl,
proxyUrlForLogs,
} from "./proxyDispatcher.ts";
import tlsClient from "./tlsClient.ts";
import { isProxyReachable } from "@/lib/proxyHealth";
function isTlsFingerprintEnabled() {
return process.env.ENABLE_TLS_FINGERPRINT === "true";
}
/** Per-request tracking of whether TLS fingerprint was used */
type TlsFingerprintStore = { used: boolean };
const tlsFingerprintContext = new AsyncLocalStorage<TlsFingerprintStore>();
type FetchWithDispatcherOptions = RequestInit & { dispatcher?: unknown };
type FetchWithDispatcher = (
input: RequestInfo | URL,
init?: FetchWithDispatcherOptions
) => Promise<Response>;
type PatchState = {
originalFetch: typeof globalThis.fetch;
proxyContext: AsyncLocalStorage<unknown>;
isPatched: boolean;
};
const isCloud = typeof caches !== "undefined" && typeof caches === "object";
const PATCH_STATE_KEY = Symbol.for("omniroute.proxyFetch.state");
function getPatchState(): PatchState {
const scopedGlobal = globalThis as typeof globalThis & {
[PATCH_STATE_KEY]?: PatchState;
};
if (!scopedGlobal[PATCH_STATE_KEY]) {
scopedGlobal[PATCH_STATE_KEY] = {
originalFetch: globalThis.fetch,
proxyContext: new AsyncLocalStorage(),
isPatched: false,
};
}
return scopedGlobal[PATCH_STATE_KEY];
}
const patchState = getPatchState();
const originalFetch = patchState.originalFetch;
const originalFetchWithDispatcher = originalFetch as FetchWithDispatcher;
const proxyContext = patchState.proxyContext;
function noProxyMatch(targetUrl) {
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
if (!noProxy) return false;
let target;
try {
target = new URL(targetUrl);
} catch {
return false;
}
const hostname = target.hostname.toLowerCase();
const port = target.port || (target.protocol === "https:" ? "443" : "80");
const patterns = noProxy
.split(",")
.map((p) => p.trim().toLowerCase())
.filter(Boolean);
return patterns.some((pattern) => {
if (pattern === "*") return true;
const [patternHost, patternPort] = pattern.split(":");
if (patternPort && patternPort !== port) return false;
if (!patternHost) return false;
// Support wildcard matching (e.g. 192.168.* or *.local)
if (patternHost.includes("*")) {
const regexStr =
"^" + patternHost.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*") + "$";
if (new RegExp(regexStr).test(hostname)) return true;
}
if (patternHost.startsWith(".")) {
return hostname.endsWith(patternHost) || hostname === patternHost.slice(1);
}
return hostname === patternHost || hostname.endsWith(`.${patternHost}`);
});
}
function isLocalAddress(hostname: string): boolean {
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") return true;
if (hostname.startsWith("192.168.")) return true;
if (hostname.startsWith("10.")) return true;
if (hostname.match(/^172\.(1[6-9]|2\d|3[0-1])\./)) return true;
if (hostname.endsWith(".local") || hostname.endsWith(".lan")) return true;
return false;
}
function resolveEnvProxyUrl(targetUrl) {
if (noProxyMatch(targetUrl)) return null;
let protocol;
try {
protocol = new URL(targetUrl).protocol;
} catch {
return null;
}
const proxyUrl =
protocol === "https:"
? process.env.HTTPS_PROXY ||
process.env.https_proxy ||
process.env.ALL_PROXY ||
process.env.all_proxy
: process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.ALL_PROXY ||
process.env.all_proxy;
if (!proxyUrl) return null;
return normalizeProxyUrl(proxyUrl, "environment proxy");
}
function resolveProxyForRequest(targetUrl) {
let target;
try {
target = new URL(targetUrl);
} catch {
target = null;
}
// Always bypass proxy for local/LAN addresses
if (target && isLocalAddress(target.hostname.toLowerCase())) {
return { source: "direct", proxyUrl: null };
}
const contextProxy = proxyContext.getStore();
if (contextProxy) {
return { source: "context", proxyUrl: proxyConfigToUrl(contextProxy) };
}
const envProxyUrl = resolveEnvProxyUrl(targetUrl);
if (envProxyUrl) {
return { source: "env", proxyUrl: envProxyUrl };
}
return { source: "direct", proxyUrl: null };
}
function getTargetUrl(input) {
if (typeof input === "string") return input;
if (input && typeof input.url === "string") return input.url;
return String(input);
}
export async function runWithProxyContext(proxyConfig, fn) {
if (typeof fn !== "function") {
throw new TypeError("runWithProxyContext requires a callback function");
}
const resolvedProxyUrl = proxyConfig ? proxyConfigToUrl(proxyConfig) : null;
// T14: Proxy Fast-Fail
// Perform a short TCP reachability check before issuing upstream requests.
if (resolvedProxyUrl) {
const reachable = await isProxyReachable(resolvedProxyUrl);
if (!reachable) {
const proxyLabel = proxyUrlForLogs(resolvedProxyUrl);
const err = new Error(`[Proxy Fast-Fail] Proxy unreachable: ${proxyLabel}`) as Error & {
code?: string;
statusCode?: number;
};
err.code = "PROXY_UNREACHABLE";
err.statusCode = 503;
throw err;
}
}
return proxyContext.run(proxyConfig || null, async () => {
if (resolvedProxyUrl) {
console.log(
`[ProxyFetch] Applied request proxy context: ${proxyUrlForLogs(resolvedProxyUrl)}`
);
}
return fn();
});
}
async function patchedFetch(input: RequestInfo | URL, options: FetchWithDispatcherOptions = {}) {
if (options?.dispatcher) {
// When a dispatcher is present, we MUST use the undici library fetch
// to ensure version compatibility. Node 22 built-in fetch (undici v6)
// is incompatible with undici v8 dispatchers (missing onRequestStart, etc.)
return (undiciFetch as unknown as (...args: unknown[]) => Promise<Response>)(input, options);
}
const targetUrl = getTargetUrl(input);
let resolved;
try {
resolved = resolveProxyForRequest(targetUrl);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[ProxyFetch] Proxy configuration error: ${message}`);
throw error;
}
const { source, proxyUrl } = resolved;
if (!proxyUrl) {
// TLS fingerprint spoofing for direct connections (no proxy configured)
if (isTlsFingerprintEnabled() && tlsClient.available) {
try {
const store = tlsFingerprintContext.getStore();
if (store) store.used = true;
return await tlsClient.fetch(targetUrl, {
...options,
headers: options.headers,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(
`[ProxyFetch] TLS fingerprint failed, falling back to native fetch: ${message}`
);
const store = tlsFingerprintContext.getStore();
if (store) store.used = false;
}
}
// Direct connection (no proxy) — use undici with custom dispatcher for timeout control.
// Falls back to original native fetch if dispatcher initialization fails (#1054).
try {
return await (undiciFetch as unknown as (...args: unknown[]) => Promise<Response>)(input, {
...options,
dispatcher: getDefaultDispatcher(),
});
} catch (dispatcherError) {
const msg =
dispatcherError instanceof Error ? dispatcherError.message : String(dispatcherError);
// CAUTION: Do NOT fallback to native fetch if the error is a version mismatch (invalid onRequestStart)
// because the native fetch will definitely fail with the undici v8 dispatcher.
if (msg.includes("onRequestStart")) {
console.error(
`[ProxyFetch] Fatal version mismatch: Dispatcher (v8) vs Fetch (v6/native). Hardware upgrade or SOCKS5 config isolation required. Error: ${msg}`
);
throw dispatcherError;
}
// Only fallback for connection/dispatcher errors, not HTTP errors
if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED") || msg.includes("UND_ERR")) {
console.warn(`[ProxyFetch] Undici dispatcher failed, falling back to native fetch: ${msg}`);
return originalFetchWithDispatcher(input, options);
}
throw dispatcherError;
}
}
try {
const dispatcher = createProxyDispatcher(proxyUrl);
return await (undiciFetch as unknown as (...args: unknown[]) => Promise<Response>)(input, {
...options,
dispatcher,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[ProxyFetch] Proxy request failed (${source}, fail-closed): ${message}`);
throw error;
}
}
if (!isCloud && !patchState.isPatched) {
globalThis.fetch = patchedFetch;
patchState.isPatched = true;
}
/**
* Run a function with TLS fingerprint tracking context.
* After fn completes, returns { result, tlsFingerprintUsed }.
*/
export async function runWithTlsTracking(fn) {
const store = { used: false };
const result = await tlsFingerprintContext.run(store, fn);
return { result, tlsFingerprintUsed: store.used };
}
/** Check if TLS fingerprint is enabled and available */
export function isTlsFingerprintActive() {
return isTlsFingerprintEnabled() && tlsClient.available;
}
export default isCloud ? originalFetch : patchedFetch;