Files
UHD_Media_Tool/apps/server/src/egress.ts
T

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>;