import { describe, expect, it, vi } from "vitest"; import { fetchPublic, isPublicHost, isPublicUrl, publicOnly, readTextCapped, UnsafeTargetError } from "#lib/publicFetch.ts"; const prod = false; describe("isPublicHost", () => { it("allows public names", () => { expect(isPublicHost("bsky.social")).toBe(true); expect(isPublicHost("pds.tngl.boltless.dev")).toBe(true); }); it("blocks ip literals, service names and internal suffixes", () => { for (const host of [ "127.0.0.1", "169.254.169.254", "[::1]", "localhost", "bobbin", "metadata.google.internal", "app.localhost", "nas.local", "vm.home.arpa", "" ]) expect(isPublicHost(host), host).toBe(false); }); it("treats a trailing dot as the same name", () => { expect(isPublicHost("localhost.")).toBe(false); expect(isPublicHost("metadata.google.internal.")).toBe(false); }); }); describe("isPublicUrl", () => { it("needs https outside dev", () => { expect(isPublicUrl(new URL("https://pds.example.com"), prod)).toBe(true); expect(isPublicUrl(new URL("http://pds.example.com"), prod)).toBe(false); // the url parser normalises numeric hosts to dotted ipv4 expect(isPublicUrl(new URL("https://2130706433"), prod)).toBe(false); }); it("lets anything through in dev", () => { expect(isPublicUrl(new URL("http://localhost:2583"), true)).toBe(true); }); }); const redirect = (to: string) => new Response(null, { status: 302, headers: { location: to } }); describe("fetchPublic", () => { it("refuses a private target without fetching", async () => { const fetch = vi.fn(); await expect( fetchPublic(fetch, "http://127.0.0.1:8090/x", {}, prod) ).rejects.toBeInstanceOf(UnsafeTargetError); expect(fetch).not.toHaveBeenCalled(); }); it("follows a redirect to another public host", async () => { const fetch = vi .fn() .mockResolvedValueOnce(redirect("https://cdn.example.com/blob")) .mockResolvedValueOnce(new Response("ok")); const res = await fetchPublic(fetch, "https://pds.example.com/x", {}, prod); expect(await res.text()).toBe("ok"); expect(fetch.mock.calls.map(([u]) => String(u))).toEqual([ "https://pds.example.com/x", "https://cdn.example.com/blob" ]); expect(fetch.mock.calls[0][1]).toMatchObject({ redirect: "manual" }); }); it("refuses a redirect into the private network", async () => { const fetch = vi.fn().mockResolvedValueOnce(redirect("http://169.254.169.254/latest")); await expect( fetchPublic(fetch, "https://pds.example.com/x", {}, prod) ).rejects.toBeInstanceOf(UnsafeTargetError); expect(fetch).toHaveBeenCalledTimes(1); }); it("gives up on a redirect loop", async () => { const fetch = vi.fn(() => Promise.resolve(redirect("https://pds.example.com/x"))); await expect(fetchPublic(fetch, "https://pds.example.com/x", {}, prod)).rejects.toThrow( /too many redirects/ ); }); }); describe("publicOnly", () => { it("rejects private targets and forbids redirects for the rest", async () => { const fetch = vi.fn(() => Promise.resolve(new Response("ok"))); const guarded = publicOnly(fetch, prod); await expect(guarded("http://10.0.0.5:9000/xrpc/x")).rejects.toBeInstanceOf( UnsafeTargetError ); await guarded("https://spindle.example.com/xrpc/x"); expect(fetch).toHaveBeenCalledTimes(1); expect(fetch.mock.calls[0]).toEqual([ "https://spindle.example.com/xrpc/x", { redirect: "error" } ]); }); }); describe("readTextCapped", () => { it("reads a body under the cap", async () => { expect(await readTextCapped(new Response("héllo"), 100)).toBe("héllo"); }); it("fails past the cap, declared or streamed", async () => { await expect( readTextCapped(new Response("x", { headers: { "content-length": "999" } }), 10) ).rejects.toThrow(/over 10 bytes/); await expect(readTextCapped(new Response("x".repeat(50)), 10)).rejects.toThrow( /over 10 bytes/ ); }); });