139 lines
5.7 KiB
TypeScript
139 lines
5.7 KiB
TypeScript
import { DEFAULT_SETTINGS } from "@umt/contracts";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import {
|
|
directHttpClient,
|
|
egressKey,
|
|
envProxyFor,
|
|
fetchVia,
|
|
noProxyMatches,
|
|
proxyHttpClient,
|
|
siteEgress,
|
|
} from "../../src/egress.ts";
|
|
import { SourcesRuntime } from "../../src/modules/sources-identity/runtime.ts";
|
|
import type { ServerContext } from "../../src/modules/types.ts";
|
|
|
|
// CF-2 出口统一:站点请求不再隐式继承容器的 HTTP(S)_PROXY。开了站点代理 → 该代理;否则明确直连。
|
|
// 本地起一个"代理"(收到请求就回 via-proxy)和一个目标(回 direct),把 HTTP_PROXY 指向前者,看请求实际走哪条。
|
|
|
|
const PROXY_KEYS = [
|
|
"HTTP_PROXY",
|
|
"HTTPS_PROXY",
|
|
"ALL_PROXY",
|
|
"http_proxy",
|
|
"https_proxy",
|
|
"all_proxy",
|
|
"NO_PROXY",
|
|
"no_proxy",
|
|
];
|
|
|
|
describe("出口选择(直连 / 代理)", () => {
|
|
let proxy: Deno.HttpServer<Deno.NetAddr>;
|
|
let target: Deno.HttpServer<Deno.NetAddr>;
|
|
let proxyUrl = "";
|
|
let targetUrl = "";
|
|
const proxied: string[] = [];
|
|
const saved = new Map<string, string | undefined>();
|
|
|
|
beforeAll(() => {
|
|
proxy = Deno.serve({ hostname: "127.0.0.1", port: 0, onListen() {} }, (req) => {
|
|
proxied.push(req.url);
|
|
return new Response("via-proxy");
|
|
});
|
|
target = Deno.serve({ hostname: "127.0.0.1", port: 0, onListen() {} }, () => new Response("direct"));
|
|
proxyUrl = `http://127.0.0.1:${proxy.addr.port}`;
|
|
targetUrl = `http://127.0.0.1:${target.addr.port}/x`;
|
|
for (const k of PROXY_KEYS) {
|
|
saved.set(k, Deno.env.get(k));
|
|
Deno.env.delete(k);
|
|
}
|
|
// 模拟容器环境:HTTP_PROXY 指向"中继",没有 NO_PROXY
|
|
Deno.env.set("HTTP_PROXY", proxyUrl);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
for (const [k, v] of saved) {
|
|
if (v === undefined) Deno.env.delete(k);
|
|
else Deno.env.set(k, v);
|
|
}
|
|
await proxy.shutdown();
|
|
await target.shutdown();
|
|
});
|
|
|
|
const body = async (f: typeof fetch) => (await f(targetUrl)).text();
|
|
|
|
it("Deno 2.9 语义:createHttpClient({}) 仍会读 HTTP_PROXY(所以直连要显式做)", async () => {
|
|
const implicit = Deno.createHttpClient({});
|
|
try {
|
|
expect(await body(fetchVia(implicit))).toBe("via-proxy");
|
|
} finally {
|
|
implicit.close();
|
|
}
|
|
});
|
|
|
|
it("directHttpClient:环境变量里有代理也直连;创建后环境变量原样还原", async () => {
|
|
proxied.length = 0;
|
|
expect(await body(fetchVia(directHttpClient()))).toBe("direct");
|
|
expect(proxied).toEqual([]);
|
|
expect(Deno.env.get("HTTP_PROXY")).toBe(proxyUrl);
|
|
});
|
|
|
|
it("proxyHttpClient:只走给定代理(NO_PROXY 命中也不绕过)", async () => {
|
|
Deno.env.set("NO_PROXY", "127.0.0.1");
|
|
try {
|
|
proxied.length = 0;
|
|
const client = proxyHttpClient(proxyUrl);
|
|
expect(await body(fetchVia(client))).toBe("via-proxy");
|
|
expect(proxied).toHaveLength(1);
|
|
expect(Deno.env.get("NO_PROXY")).toBe("127.0.0.1");
|
|
} finally {
|
|
Deno.env.delete("NO_PROXY");
|
|
}
|
|
});
|
|
|
|
it("SourcesRuntime.siteFetch:站点没开代理 → 直连;开了且设了代理地址 → 站点代理", async () => {
|
|
const ctx = { logger: { debug() {}, info() {}, warn() {} } } as unknown as ServerContext;
|
|
const rt = new SourcesRuntime(ctx);
|
|
const defaults = { ...DEFAULT_SETTINGS.sites, proxyUrl };
|
|
proxied.length = 0;
|
|
expect(await body(rt.siteFetch({ proxy: false }, defaults, "UA"))).toBe("direct");
|
|
expect(proxied).toEqual([]);
|
|
expect(await body(rt.siteFetch({ proxy: true }, defaults, "UA"))).toBe("via-proxy");
|
|
expect(proxied).toHaveLength(1);
|
|
// 开了代理但没设代理地址 → 直连
|
|
expect(await body(rt.siteFetch({ proxy: true }, { ...defaults, proxyUrl: null }, "UA"))).toBe("direct");
|
|
expect(rt.egressKeyOf({ proxy: false }, defaults)).toBe("direct");
|
|
expect(rt.egressKeyOf({ proxy: true }, defaults)).toBe(`proxy:http://127.0.0.1:${proxy.addr.port}`);
|
|
});
|
|
});
|
|
|
|
describe("外网请求按环境变量(行为不变,改为显式)", () => {
|
|
it("HTTPS / HTTP / ALL_PROXY 与 NO_PROXY", () => {
|
|
const env = { HTTPS_PROXY: "http://relay:7891", HTTP_PROXY: "http://relay:7890", NO_PROXY: "localhost,.lan" };
|
|
expect(envProxyFor(new URL("https://api.themoviedb.org/3"), env)).toBe("http://relay:7891");
|
|
expect(envProxyFor(new URL("http://image.tmdb.org/t"), env)).toBe("http://relay:7890");
|
|
expect(envProxyFor(new URL("http://nas.lan/x"), env)).toBeNull();
|
|
expect(envProxyFor(new URL("https://api.search.brave.com/"), { ALL_PROXY: "socks5h://relay:1080" })).toBe(
|
|
"socks5h://relay:1080",
|
|
);
|
|
expect(envProxyFor(new URL("https://api.search.brave.com/"), {})).toBeNull();
|
|
expect(envProxyFor(new URL("https://x.example/"), { https_proxy: "http://lower:1" })).toBe("http://lower:1");
|
|
});
|
|
it("NO_PROXY 匹配", () => {
|
|
expect(noProxyMatches("a.example.com", "example.com")).toBe(true);
|
|
expect(noProxyMatches("example.com", ".example.com")).toBe(true);
|
|
expect(noProxyMatches("badexample.com", "example.com")).toBe(false);
|
|
expect(noProxyMatches("x", "*")).toBe(true);
|
|
expect(noProxyMatches("127.0.0.1", "localhost, 127.0.0.1:8080")).toBe(true);
|
|
});
|
|
it("出口键不含代理口令", () => {
|
|
expect(egressKey(siteEgress(false, "http://u:p@relay:7891"))).toBe("direct");
|
|
expect(egressKey(siteEgress(true, "http://u:p@relay:7891"))).toBe("proxy:http://relay:7891");
|
|
expect(egressKey(siteEgress(true, null, {}))).toBe("direct");
|
|
// 设置里没填站点代理地址:沿用进程的 HTTPS_PROXY
|
|
expect(egressKey(siteEgress(true, null, { HTTPS_PROXY: "http://172.17.0.1:7891" }))).toBe(
|
|
"proxy:http://172.17.0.1:7891",
|
|
);
|
|
expect(egressKey(siteEgress(false, null, { HTTPS_PROXY: "http://172.17.0.1:7891" }))).toBe("direct");
|
|
});
|
|
});
|