98 lines
3.9 KiB
TypeScript
98 lines
3.9 KiB
TypeScript
// 每次请求显式指定 Bun 的 proxy,直连使用proxy: false以禁用环境代理。
|
|
export interface HttpClient {
|
|
proxy: false | { url: string; respectNoProxy: false };
|
|
close(): void;
|
|
}
|
|
const direct: HttpClient = { proxy: false, close() {} };
|
|
const proxied = new Map<string, HttpClient>();
|
|
const ENV_SNAPSHOT = { ...process.env };
|
|
export function directHttpClient(): HttpClient {
|
|
return direct;
|
|
}
|
|
export function proxyHttpClient(url: string): HttpClient {
|
|
let client = proxied.get(url);
|
|
if (!client) {
|
|
client = { proxy: { url, respectNoProxy: false }, close() {} };
|
|
proxied.set(url, client);
|
|
}
|
|
return client;
|
|
}
|
|
export function fetchVia(client: HttpClient): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {
|
|
return ((input: RequestInfo | URL, init?: RequestInit) =>
|
|
globalThis.fetch(input, { ...init, proxy: client.proxy } as RequestInit)) as (
|
|
input: RequestInfo | URL,
|
|
init?: RequestInit,
|
|
) => Promise<Response>;
|
|
}
|
|
|
|
/** 站点请求的出口:开代理 → 设置里的站点代理地址,没填则用进程的 HTTPS_PROXY / HTTP_PROXY;都没有 → direct */
|
|
export interface SiteEgress {
|
|
mode: "direct" | "proxy";
|
|
proxyUrl: string | null;
|
|
}
|
|
|
|
export function siteEgress(
|
|
proxy: boolean,
|
|
proxyUrl: string | null | undefined,
|
|
env: Record<string, string | undefined> = ENV_SNAPSHOT,
|
|
): SiteEgress {
|
|
const url = proxy ? proxyUrl || env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy : null;
|
|
return url ? { mode: "proxy", proxyUrl: url } : { mode: "direct", proxyUrl: null };
|
|
}
|
|
|
|
/** 出口的比较键(不含代理口令):direct / proxy:<主机:端口> */
|
|
export function egressKey(e: SiteEgress): string {
|
|
if (e.mode === "direct" || !e.proxyUrl) return "direct";
|
|
try {
|
|
const u = new URL(e.proxyUrl);
|
|
return `proxy:${u.protocol}//${u.host}`;
|
|
} catch {
|
|
return "proxy:?";
|
|
}
|
|
}
|
|
|
|
export function siteFetchFor(e: SiteEgress): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {
|
|
return fetchVia(e.mode === "proxy" && e.proxyUrl ? proxyHttpClient(e.proxyUrl) : directHttpClient());
|
|
}
|
|
|
|
// ---------- 外网请求(TMDB / Brave / 图片):显式按环境变量 ----------
|
|
|
|
/** NO_PROXY 匹配(与常见实现一致:逗号分隔;`.a.com` / `a.com` 匹配自身与子域;`*` 匹配全部;IP 精确匹配) */
|
|
export function noProxyMatches(host: string, noProxy: string | undefined): boolean {
|
|
if (!noProxy) return false;
|
|
const h = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
for (const raw of noProxy.split(",")) {
|
|
const p = raw.trim().toLowerCase().replace(/:\d+$/, "");
|
|
if (!p) continue;
|
|
if (p === "*") return true;
|
|
const d = p.replace(/^\*?\./, "");
|
|
if (h === d || h.endsWith(`.${d}`)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** 外网请求按环境变量应走的代理(进程启动时的快照);没有 / NO_PROXY 命中为 null */
|
|
export function envProxyFor(target: URL, env: Record<string, string | undefined> = ENV_SNAPSHOT): string | null {
|
|
const get = (k: string) => env[k] ?? env[k.toLowerCase()];
|
|
if (noProxyMatches(target.hostname, get("NO_PROXY"))) return null;
|
|
const byScheme = target.protocol === "https:" ? get("HTTPS_PROXY") : get("HTTP_PROXY");
|
|
const p = byScheme || get("ALL_PROXY");
|
|
return p ? p : null;
|
|
}
|
|
|
|
/** 外网请求用的 fetch:按环境变量显式选代理 / 直连(与之前隐式继承的行为一致) */
|
|
export const externalFetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = ((
|
|
input: RequestInfo | URL,
|
|
init?: RequestInit,
|
|
) => {
|
|
let target: URL | null = null;
|
|
try {
|
|
target = new URL(input instanceof Request ? input.url : String(input));
|
|
} catch {
|
|
// 交给 fetch 报错
|
|
}
|
|
const proxy = target ? envProxyFor(target) : null;
|
|
const client = proxy ? proxyHttpClient(proxy) : directHttpClient();
|
|
return fetchVia(client)(input, init);
|
|
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|