import { test } from "node:test"; import assert from "node:assert/strict"; import { isIP } from "node:net"; import { readFile } from "node:fs/promises"; import vm from "node:vm"; import ts from "typescript"; const environment = { PUBLIC_DATA_CACHE_TTL_MS: "60000" }; const sharedDependencies = { "node:net": { isIP } }; async function compile(path, dependencies = {}, extra = {}) { const exports = {}; const { outputText } = ts.transpileModule( await readFile( new URL("../src/lib/api/" + path + ".ts", import.meta.url), "utf8", ), { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS, }, }, ); vm.runInNewContext(outputText, { exports, URL, Headers, Request, Response, FormData, AbortSignal, TextDecoder, Buffer, structuredClone, process: { env: environment }, require: (name) => { if (name in dependencies) return dependencies[name]; if (name in sharedDependencies) return sharedDependencies[name]; throw new Error("Unknown dependency: " + name); }, ...extra, }); return exports; } const config = await compile("config"); const protocol = await compile("protocol"); const cache = await compile("public-cache"); sharedDependencies["./user-agent"] = await compile("user-agent"); sharedDependencies["./sdk-headers"] = await compile("sdk-headers"); sharedDependencies["./client-headers"] = await compile("client-headers"); sharedDependencies["../site-navigation"] = await compile("../site-navigation", { "./api/protocol": protocol, "./routing": await compile("../routing"), }); test("development uses its configured domain and supports legacy overrides", () => { delete environment.UPSTREAM_API_URL; delete environment.API_BASE_URL; assert.equal(config.getApiBaseUrl(), "http://172.104.181.99:26792"); environment.APP_ENV = "test"; assert.equal(config.getApiBaseUrl(), "http://172.104.181.99:26792"); environment.APP_ENV = "production"; assert.equal(config.getApiBaseUrl(), "http://139.162.49.252:21040"); delete environment.APP_ENV; environment.API_BASE_URL = "https://legacy-api.example/"; assert.equal(config.getApiBaseUrl(), "https://legacy-api.example"); environment.UPSTREAM_API_URL = " http://127.0.0.1:21111/ "; assert.equal(config.getApiBaseUrl(), "http://127.0.0.1:21111"); for (const value of [ "", "javascript:bad", "https://user:secret@api.example", "https://api.example/?query=1", "https://api.example/#fragment", ]) { environment.UPSTREAM_API_URL = value; assert.throws(() => config.getApiBaseUrl(), /UPSTREAM_API_URL/); } }); test("explicit current mode resolves the current domain per request, including protocol and port", () => { environment.UPSTREAM_API_URL = "current"; delete environment.API_BASE_URL; for (const APP_ENV of ["test", "production"]) { environment.APP_ENV = APP_ENV; assert.equal(config.usesCurrentApiOrigin(), true); assert.equal( config.getApiBaseUrl({ url: "http://0.0.0.0:21030/api/app/config/info", headers: new Headers({ Host: "first.example:8443", "X-Forwarded-Proto": "https", "X-Forwarded-Host": "ignored.example", }), }), "https://first.example:8443", ); assert.equal( config.getApiBaseUrl({ url: "http://second.example:21030/api/app/config/info", headers: new Headers(), }), "http://second.example:21030", ); assert.equal( config.getApiBaseUrl({ headers: new Headers({ "X-Forwarded-Host": "fallback.example", "X-Forwarded-Proto": "https", }), }), "https://fallback.example", ); assert.throws(() => config.getApiBaseUrl(), /请求域名/); for (const Host of [ "user:secret@api.test", "api.test/path", "api.test?x=1", "api.test,other.test", "api.test\\other", ]) { assert.throws(() => config.getApiBaseUrl({ headers: new Headers({ Host }) }), ); } assert.throws(() => config.getApiBaseUrl({ headers: new Headers({ Host: "api.test", "X-Forwarded-Proto": "ftp" }), }), ); } delete environment.APP_ENV; }); test("SSR and API proxy share the configured upstream while cache entries stay isolated by backend", async () => { const requests = []; const incoming = new Headers({ "User-Agent": "visitor-ua", "X-Real-IP": "203.0.113.9", }); const server = await compile( "server", { "./protocol": protocol, "./config": config, "./public-cache": cache, react: { cache: (fn) => fn }, "next/headers": { headers: async () => incoming }, }, { fetch: async (url, options) => { requests.push({ url: String(url), ...options }); return Response.json({ code: 200, data: { source: new URL(url).origin }, }); }, }, ); environment.UPSTREAM_API_URL = "https://api-a.example"; const first = await server.upstream("config/info"); assert.equal(first.data.source, "https://api-a.example"); assert.equal(requests[0].headers["User-Agent"], "visitor-ua"); assert.equal(requests[0].headers["X-Real-IP"], "203.0.113.9"); environment.UPSTREAM_API_URL = "https://api-b.example"; assert.equal( (await server.upstream("config/info")).data.source, "https://api-b.example", ); environment.UPSTREAM_API_URL = "https://api-a.example"; assert.equal( (await server.upstream("config/info")).data.source, "https://api-a.example", ); assert.equal(requests.length, 2); await server.upstream("auth/getInfo", { token: "server-only-token" }); assert.equal( requests.at(-1).url, "https://api-a.example/api/app/auth/getInfo", ); assert.equal(requests.at(-1).headers.Authorization, "server-only-token"); }); test("SSR, API handlers and private requests use the current origin without mixing public caches", async () => { environment.UPSTREAM_API_URL = "current"; let incoming = new Headers({ Host: "site-a.example", "X-Forwarded-Proto": "https", }); const requests = []; const server = await compile( "server", { "./protocol": protocol, "./config": config, "./public-cache": cache, react: { cache: (fn) => fn }, "next/headers": { headers: async () => incoming }, }, { fetch: async (url, options) => { requests.push({ url: String(url), ...options }); return Response.json({ code: 200, data: { source: new URL(url).origin }, }); }, }, ); assert.equal( (await server.upstream("config/info")).data.source, "https://site-a.example", ); incoming = new Headers({ Host: "site-b.example", "X-Forwarded-Proto": "http", }); assert.equal( (await server.upstream("config/info")).data.source, "http://site-b.example", ); const request = new Request("https://site-a.example/api/app/config/info"); assert.equal( (await server.upstream("config/info", { request })).data.source, "https://site-a.example", ); assert.equal(requests.length, 2); await server.upstream("auth/getInfo", { request, token: "private-current-token", }); assert.equal( requests.at(-1).url, "https://site-a.example/api/app/auth/getInfo", ); assert.equal(requests.at(-1).headers.Authorization, "private-current-token"); assert.equal(requests.at(-1).headers[config.UPSTREAM_HOP_HEADER], "1"); }); test("current-origin media preserves signatures and fails immediately if the upstream returns to Next", async () => { environment.UPSTREAM_API_URL = "current"; const requests = []; const media = await compile( "media", { "node:net": { isIP }, "./config": config }, { fetch: async (url, options) => { requests.push({ url: String(url), ...options }); return new Response(new Uint8Array(16)); }, }, ); const request = new Request( "http://0.0.0.0:21030/enkey?id=1&ticket=a%2Fb+1", { headers: { Host: "deployed.example", "X-Forwarded-Proto": "https", Cookie: "drama_session=secret", }, }, ); assert.equal((await media.proxyMedia(request, "/enkey", true)).status, 200); assert.equal( requests[0].url, "https://deployed.example/enkey?id=1&ticket=a%2Fb+1", ); assert.equal(requests[0].headers.get("cookie"), null); assert.equal(requests[0].headers.get(config.UPSTREAM_HOP_HEADER), "1"); const loop = new Request("https://deployed.example/enkey", { headers: { [config.UPSTREAM_HOP_HEADER]: "1" }, }); const response = await media.proxyMedia(loop, "/enkey", true); assert.equal(response.status, 502); assert.match(await response.text(), /转发配置/); assert.equal(requests.length, 1); }); test("media and keys use the configured upstream while preserving signatures, byte ranges and credential isolation", async () => { const reads = []; const media = await compile( "media", { "node:net": { isIP }, "./config": config }, { fetch: async (url, options) => { reads.push({ url: String(url), headers: options.headers }); return new Response( new URL(url).pathname === "/enkey/" ? new Uint8Array(16) : "#EXTM3U\n#EXT-X-ENDLIST\n", ); }, }, ); environment.UPSTREAM_API_URL = "http://127.0.0.1:21111"; const incoming = new Request( "https://current-site.example/h5/m3u8/video.m3u8?md=a%2Fb+value&x=1&x=2", { headers: { Range: "bytes=0-2", Cookie: "drama_session=secret", Authorization: "secret", "X-Real-IP": "203.0.113.4", }, }, ); assert.equal( (await media.proxyMedia(incoming, "/h5/m3u8/video.m3u8")).status, 200, ); assert.equal( reads.at(-1).url, "http://127.0.0.1:21111/h5/m3u8/video.m3u8?md=a%2Fb+value&x=1&x=2", ); assert.equal(reads.at(-1).headers.get("range"), "bytes=0-2"); assert.equal(reads.at(-1).headers.get("x-real-ip"), "203.0.113.4"); assert.equal(reads.at(-1).headers.get("x-forwarded-for"), null); assert.equal(reads.at(-1).headers.get("authorization"), null); assert.equal(reads.at(-1).headers.get("cookie"), null); const key = await media.proxyMedia( new Request("https://current-site.example/enkey?token=a%2Fb"), "/enkey/", true, ); assert.equal(key.status, 200); assert.equal((await key.arrayBuffer()).byteLength, 16); assert.equal(reads.at(-1).url, "http://127.0.0.1:21111/enkey/?token=a%2Fb"); }); test("key proxies follow one same-origin slash change in either direction without changing signed queries or headers", async () => { environment.UPSTREAM_API_URL = "https://key-api.example"; const query = "?id=12700&t=123&ticket=a%2Fb+1&ticket=x%252Fy"; for (const pathname of ["/enkey", "/enkey/", "/newenkey", "/newenkey/"]) { const alternate = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname + "/"; const reads = []; const media = await compile( "media", { "node:net": { isIP }, "./config": config }, { fetch: async (url, options) => { reads.push({ url: String(url), ...options }); return reads.length === 1 ? new Response("", { status: 301, headers: { Location: alternate + (pathname.endsWith("/") ? "" : query), }, }) : new Response(new Uint8Array(16)); }, }, ); const result = await media.proxyMedia( new Request("http://local.test" + pathname + query, { headers: { "X-Real-IP": "203.0.113.7", "User-Agent": "key-test", Cookie: "drama_session=private", Authorization: "private", }, }), pathname, true, ); assert.equal(result.status, 200); assert.equal((await result.arrayBuffer()).byteLength, 16); assert.deepEqual( reads.map((item) => item.url), [ "https://key-api.example" + pathname + query, "https://key-api.example" + alternate + query, ], ); for (const read of reads) { assert.equal(read.redirect, "manual"); assert.equal(read.cache, "no-store"); assert.equal(read.headers.get("X-Real-IP"), "203.0.113.7"); assert.equal(read.headers.get("User-Agent"), "key-test"); assert.equal( read.headers.get("X-User-Agent"), "DevType=desktop;SysType=h5_pc;Ver=1.0.0;BuildID=;device_brand=;device_model=;system_name=Other;system_version=;sid=", ); assert.equal(read.headers.get("Cookie"), null); assert.equal(read.headers.get("Authorization"), null); } assert.equal(reads[0].signal, reads[1].signal); } }); test("key proxies reject unrelated, cross-origin and repeated redirects before forwarding credentials or signatures", async () => { environment.UPSTREAM_API_URL = "https://key-api.example"; for (const [location, repeated] of [ ["https://external.example/enkey/", false], ["/newenkey/", false], ["/enkey/?ticket=changed", false], ["https://user:secret@key-api.example/enkey/", false], ["/enkey/", true], [null, false], ]) { const reads = []; const media = await compile( "media", { "node:net": { isIP }, "./config": config }, { fetch: async (url) => { reads.push(String(url)); return new Response("", { status: 302, headers: location ? { Location: reads.length === 1 ? location : "/enkey" } : {}, }); }, }, ); const result = await media.proxyMedia( new Request("http://local.test/enkey?ticket=original"), "/enkey", true, ); assert.equal(result.status, 502, String(location)); assert.equal(reads.length, repeated ? 2 : 1, String(location)); } });