Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380import { isRedirect } from "@sveltejs/kit";import type { ResolveOptions } from "@sveltejs/kit/hooks";import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";import { createSecureServer, type Http2Session } from "node:http2";import type { AddressInfo } from "node:net";import { Agent, fetch as undiciFetch } from "undici";import { handle, handleFetch, upstreamPool } from "./hooks.server";
const config = vi.hoisted(() => ({ sitemapUrl: "", bobbinUrl: "https://bobbin.test", avatarUrl: "https://avatar.test", camoUrl: "https://camo.test", upstreamTimeoutMs: 15_000}));vi.mock("#lib/server/config.ts", () => ({ getConfig: () => config }));const repos = vi.hoisted(() => ({ resolveRepoByRepoDid: vi.fn() }));vi.mock("#lib/api/repo.ts", () => repos);
const run = ( path: string, binding?: (request: Request) => Promise<Response>, routeId: string | null = null) => { const resolve = vi.fn(async () => new Response("page")); const url = new URL(`https://next.tangled.org${path}`); const event = { url, route: { id: routeId }, fetch: vi.fn(), request: new Request(url), platform: binding ? { env: { SITEMAP: { fetch: binding } } } : undefined }; return { res: handle({ event, resolve } as unknown as Parameters<typeof handle>[0]), resolve };};
beforeEach(() => { config.sitemapUrl = ""; repos.resolveRepoByRepoDid.mockReset().mockResolvedValue(null); vi.spyOn(console, "warn").mockImplementation(() => {});});afterEach(() => vi.unstubAllGlobals());
describe("handle: legacy redirects", () => { it.each([ ["/@alice.test", 302, "/alice.test"], ["/@alice.test/core/tree/main?x=1", 302, "/alice.test/core/tree/main?x=1"], ["/%40alice.test/core", 302, "/alice.test/core"], [ "/@tangled.org/core/info/refs?service=git-upload-pack", 302, "/tangled.org/core/info/refs?service=git-upload-pack" ], ["/tangled.sh", 301, "/tangled.org"], ["/@tangled.sh", 301, "/tangled.org"], ["/tangled.sh/core/issues?state=open", 301, "/tangled.org/core/issues?state=open"], ["/@tangled.sh/core", 301, "/tangled.org/core"] ])("sends %s to %s %s", async (path, status, location) => { const { res, resolve } = run(path); const out = await res; expect([out.status, out.headers.get("location")]).toEqual([status, location]); expect(resolve).not.toHaveBeenCalled(); });
it.each([ "/tangled.shop/core", "/tangled.org/core", "/did:plc:j5hmlfdrwkvtxm7cjmu7j2is/info/refs", "/@did:plc:j5hmlfdrwkvtxm7cjmu7j2is", "/@", "/@not a handle", "/settings", "/login", "/alice.test/@core", "/%E0%A4%A/core", "/%zz" ])("leaves %s to the routes", async (path) => { const { res, resolve } = run(path); await res; expect(resolve).toHaveBeenCalled(); });});
describe("handle: repo did permalinks", () => { const repoDid = "did:plc:j5hmlfdrwkvtxm7cjmu7j2is"; const redirected = async (res: ReturnType<typeof handle>) => { try { await res; } catch (thrown) { if (isRedirect(thrown)) return [thrown.status, thrown.location]; throw thrown; } return null; };
beforeEach(() => { repos.resolveRepoByRepoDid.mockImplementation(async (_, did: string) => did === repoDid ? { owner: "tangled.org", name: "core" } : null ); });
it.each([ ["", "/tangled.org/core"], ["/issues", "/tangled.org/core/issues"], ["/issues/12", "/tangled.org/core/issues/12"], ["/tree/main/web/src?x=1", "/tangled.org/core/tree/main/web/src?x=1"] ])("sends the repo did%s to the repo", async (rest, location) => { const { res, resolve } = run(`/${repoDid}${rest}`); expect(await redirected(res)).toEqual([307, location]); expect(resolve).not.toHaveBeenCalled(); });
it.each(["/did:plc:aaaaaaaaaaaaaaaaaaaaaaaa", "/did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/core"])( "leaves %s, a did that isn't a repo, to the routes", async (path) => { const { res, resolve } = run(path); expect(await redirected(res)).toBeNull(); expect(resolve).toHaveBeenCalled(); } );
it("leaves git's info/refs at the permalink to its route without a lookup", async () => { const { res, resolve } = run(`/${repoDid}/info/refs`, undefined, "/[handle]/info/refs"); expect(await redirected(res)).toBeNull(); expect(resolve).toHaveBeenCalled(); expect(repos.resolveRepoByRepoDid).not.toHaveBeenCalled(); });
it("doesn't look up a handle", async () => { const { res } = run("/tangled.org/core"); await res; expect(repos.resolveRepoByRepoDid).not.toHaveBeenCalled(); });});
describe("handle: sitemaps", () => { it("prefers the worker binding", async () => { config.sitemapUrl = "https://sitemap.test"; const binding = vi.fn(async () => new Response("bound")); const network = vi.fn(); vi.stubGlobal("fetch", network); const { res } = run("/sitemap.xml", binding); await expect((await res).text()).resolves.toBe("bound"); expect(network).not.toHaveBeenCalled(); });
it.each(["/sitemap.xml", "/sitemaps/repos-3.xml?v=2"])( "fetches %s from SITEMAP_URL without a binding, dropping the decoded encoding", async (path) => { config.sitemapUrl = "https://sitemap.test"; const network = vi.fn( async (_: string) => new Response("<urlset/>", { headers: { "content-type": "application/xml", "content-encoding": "gzip", "content-length": "9" } }) ); vi.stubGlobal("fetch", network); const { res, resolve } = run(path); const out = await res; expect(network).toHaveBeenCalledWith(`https://sitemap.test${path}`); expect(resolve).not.toHaveBeenCalled(); await expect(out.text()).resolves.toBe("<urlset/>"); expect(out.headers.get("content-type")).toBe("application/xml"); expect(out.headers.get("content-encoding")).toBeNull(); expect(out.headers.get("content-length")).toBeNull(); } );
it("falls through to the app with neither", async () => { const network = vi.fn(); vi.stubGlobal("fetch", network); const { res, resolve } = run("/sitemap.xml"); await expect((await res).text()).resolves.toBe("page"); expect(resolve).toHaveBeenCalledOnce(); expect(network).not.toHaveBeenCalled(); });});
describe("handle: security headers", () => { const page = async (response: Response) => { const url = new URL("https://next.tangled.org/someone/repo"); const event = { url, request: new Request(url), platform: undefined }; const resolve = vi.fn(async () => response); return handle({ event, resolve } as unknown as Parameters<typeof handle>[0]); };
it("adds them to a rendered response", async () => { const res = await page(new Response("page")); expect(Object.fromEntries(res.headers)).toMatchObject({ "x-content-type-options": "nosniff", "referrer-policy": "strict-origin-when-cross-origin", "x-frame-options": "DENY", "content-security-policy": "frame-ancestors 'none'" }); await expect(res.text()).resolves.toBe("page"); });
it("keeps a header the route set itself", async () => { const own = "sandbox; default-src 'none'"; const res = await page( new Response("raw", { headers: { "content-security-policy": own } }) ); expect(res.headers.get("content-security-policy")).toBe(own); expect(res.headers.get("x-frame-options")).toBe("DENY"); });
it("copies a response whose headers can't be changed", async () => { const res = await page(Response.redirect("https://cdn.example.com/a", 302)); expect(res.status).toBe(302); expect(res.headers.get("location")).toBe("https://cdn.example.com/a"); expect(res.headers.get("x-content-type-options")).toBe("nosniff"); });});
describe("handle: page head", () => { it("preconnects to the configured avatar and camo services", async () => { const url = new URL("https://next.tangled.org/someone/repo"); const event = { url, request: new Request(url), platform: undefined }; const resolve = vi.fn(async (_: unknown, opts: ResolveOptions) => { const html = '<link href="%tangled.avatar%" /><link href="%tangled.camo%" />'; return new Response(await opts.transformPageChunk?.({ html, done: true })); }); const res = await handle({ event, resolve } as unknown as Parameters<typeof handle>[0]); await expect(res.text()).resolves.toBe( '<link href="https://avatar.test" /><link href="https://camo.test" />' ); });});
describe("handleFetch", () => { beforeEach(() => vi.useFakeTimers()); afterEach(() => { vi.useRealTimers(); config.upstreamTimeoutMs = 15_000; });
const call = async (request: Request, fetch: (request: Request) => Promise<Response>) => handleFetch({ request, fetch } as unknown as Parameters<typeof handleFetch>[0]);
const hanging = (request: Request) => new Promise<Response>((_, reject) => request.signal.addEventListener("abort", () => reject(request.signal.reason)) );
it("gives up on a get that has no answer in time", async () => { const out = call(new Request("https://bobbin.test/xrpc/x"), hanging); const failed = expect(out).rejects.toThrow(); await vi.advanceTimersByTimeAsync(15_000); await failed; expect(console.warn).toHaveBeenCalledWith( "[hooks.server] bobbin.test/xrpc/x gave no answer in 15000 ms" ); });
it("leaves a body that is still streaming once the headers came", async () => { let signal: AbortSignal | undefined; const out = await call(new Request("https://bobbin.test/xrpc/x"), async (request) => { signal = request.signal; return new Response("ok"); }); await vi.advanceTimersByTimeAsync(60_000); expect(signal?.aborted).toBe(false); expect(await out.text()).toBe("ok"); });
it("doesn't bound a push or clone post", async () => { const out = call( new Request("https://knot.test/git-receive-pack", { method: "POST" }), hanging ); let settled = false; void out.then( () => (settled = true), () => (settled = true) ); await vi.advanceTimersByTimeAsync(60_000); expect(settled).toBe(false); });
it("waits as long as it takes when the timeout is 0", async () => { config.upstreamTimeoutMs = 0; const out = call(new Request("https://bobbin.test/xrpc/x"), hanging); let settled = false; void out.then( () => (settled = true), () => (settled = true) ); await vi.advanceTimersByTimeAsync(60_000); expect(settled).toBe(false); });});
// self-signed for 127.0.0.1, only ever served inside this testconst TEST_CERT = `-----BEGIN CERTIFICATE-----MIIBmzCCAUGgAwIBAgIUfes5w7WyBUNKaPeKwxWOEvao70IwCgYIKoZIzj0EAwIwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MTAyOTIyMzEzNVoYDzIxMjYxMDA1MjIzMTM1WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASIWcZ5H+k949hDyfpHuWzsHPhx29HL/vLL0G7DMGPIDZ2A00D/Dkl61xoFynaNvcMkGTCDSTniIg1R3XIAbSuWo28wbTAdBgNVHQ4EFgQU0cTqOs5nXWSom1T+lGfPXCk7HMwwHwYDVR0jBBgwFoAU0cTqOs5nXWSom1T+lGfPXCk7HMwwDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwCgYIKoZIzj0EAwIDSAAwRQIgIeV3oKSwakMNd+bG3H3XCAU9cJeX6B/t9yoOJd6pWPYCIQCAbgl5XnEX5+i6Sa42SS3HW4bY+vz6nFDK2lDu2MLI8w==-----END CERTIFICATE-----`;const TEST_KEY = `-----BEGIN PRIVATE KEY-----MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgoxzF7/Ony88kSNqqAp1tbQjKUE2/fYeUqDlFVUBzXeuhRANCAASIWcZ5H+k949hDyfpHuWzsHPhx29HL/vLL0G7DMGPIDZ2A00D/Dkl61xoFynaNvcMkGTCDSTniIg1R3XIAbSuW-----END PRIVATE KEY-----`;
describe("upstreamPool", () => { const ourOrigin = (port: number) => `https://127.0.0.1:${port}`;
// counts connections and the most requests one connection carried at once const h2Server = async () => { const sessions = new Map<Http2Session, number>(); let most = 0; const server = createSecureServer({ cert: TEST_CERT, key: TEST_KEY }, (req, res) => { const session = req.stream.session!; const now = (sessions.get(session) ?? 0) + 1; sessions.set(session, now); most = Math.max(most, now); setTimeout(() => { sessions.set(session, sessions.get(session)! - 1); res.end("ok"); }, 50); }); await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve)); const port = (server.address() as AddressInfo).port; return { port, stats: () => ({ connections: sessions.size, most }), close: () => server.close() }; };
const burst = async (agent: Agent, url: string, count: number) => { const out = await Promise.all( Array.from({ length: count }, () => undiciFetch(url, { dispatcher: agent }).then((res) => res.text()) ) ); expect(out.every((body) => body === "ok")).toBe(true); };
const agentFor = (ours: string[]) => new Agent({ factory: upstreamPool(new Set(ours)), connect: { rejectUnauthorized: false } });
// a cold burst still opens a connection per request, since none knows it got http/2 yet it("spreads an http/2 host that isn't ours over streams once it's connected", async () => { const server = await h2Server(); const agent = agentFor([]); try { await burst(agent, ourOrigin(server.port), 1); await burst(agent, ourOrigin(server.port), 50); expect(server.stats().connections).toBe(1); expect(server.stats().most).toBeGreaterThan(1); } finally { await agent.close(); server.close(); } });
it("keeps our own services to a few http/2 connections", async () => { const server = await h2Server(); const agent = agentFor([ourOrigin(server.port)]); try { await burst(agent, ourOrigin(server.port), 300); expect(server.stats().connections).toBeLessThanOrEqual(8); } finally { await agent.close(); server.close(); } });});