import { describe, expect, it } from "vitest"; import { assertUrlAllowed, isBlockedIP, isBlockedHostname, FetchaError, fetchRequestSchema, generateApiKey, hashApiKey, looksBlocked, htmlToText, normalizeGeo } from "../src/index"; describe("SSRF protection", () => { it("blocks private and reserved IPv4 ranges", () => { for (const ip of ["127.0.0.1", "10.1.2.3", "172.16.0.1", "172.31.255.255", "192.168.1.1", "169.254.169.254", "0.0.0.0", "100.64.0.1", "224.0.0.1", "255.255.255.255"]) { expect(isBlockedIP(ip), ip).toBe(true); } for (const ip of ["8.8.8.8", "1.1.1.1", "93.184.216.34", "172.32.0.1", "11.0.0.1"]) { expect(isBlockedIP(ip), ip).toBe(false); } }); it("blocks IPv6 loopback, link-local, ULA and v4-mapped private", () => { for (const ip of ["::1", "::", "fe80::1", "fd00::1", "fc00::1", "::ffff:127.0.0.1", "::ffff:10.0.0.1", "ff02::1"]) { expect(isBlockedIP(ip), ip).toBe(true); } expect(isBlockedIP("2606:4700:4700::1111")).toBe(false); }); it("blocks internal hostnames", () => { for (const h of ["localhost", "LOCALHOST.", "foo.local", "metadata.google.internal", "api.internal", "intranet", "m3u96a.maclustr.io"]) { expect(isBlockedHostname(h), h).toBe(true); } expect(isBlockedHostname("example.com")).toBe(false); }); it("rejects non-http schemes and credentials", async () => { for (const u of ["file:///etc/passwd", "ftp://example.com/x", "gopher://x", "data:text/html,hi", "javascript:alert(1)"]) { await expect(assertUrlAllowed(u, { resolve: false })).rejects.toMatchObject({ code: "URL_NOT_ALLOWED" }); } await expect(assertUrlAllowed("https://user:pw@example.com/", { resolve: false })).rejects.toMatchObject({ code: "URL_NOT_ALLOWED" }); await expect(assertUrlAllowed("not a url", { resolve: false })).rejects.toMatchObject({ code: "INVALID_REQUEST" }); }); it("rejects literal private IPs and numeric encodings", async () => { await expect(assertUrlAllowed("http://169.254.169.254/latest/meta-data")).rejects.toBeInstanceOf(FetchaError); await expect(assertUrlAllowed("http://[::1]:8080/")).rejects.toMatchObject({ code: "URL_NOT_ALLOWED" }); await expect(assertUrlAllowed("http://2130706433/")).rejects.toMatchObject({ code: "URL_NOT_ALLOWED" }); await expect(assertUrlAllowed("http://0x7f000001/")).rejects.toMatchObject({ code: "URL_NOT_ALLOWED" }); }); it("allows public URLs without resolving when asked", async () => { const r = await assertUrlAllowed("https://example.com/path?q=1", { resolve: false }); expect(r.hostname).toBe("example.com"); }); }); describe("request schema", () => { it("applies defaults and uppercases country", () => { const r = fetchRequestSchema.parse({ url: "https://example.com", country: "ca" }); expect(r.method).toBe("GET"); expect(r.network).toBe("auto"); expect(r.timeout).toBe(30_000); expect(r.country).toBe("CA"); expect(r.follow_redirects).toBe(true); }); it("rejects unknown fields and bad values", () => { expect(fetchRequestSchema.safeParse({ url: "https://e.com", foo: 1 }).success).toBe(false); expect(fetchRequestSchema.safeParse({ url: "https://e.com", timeout: 10 }).success).toBe(false); expect(fetchRequestSchema.safeParse({ url: "https://e.com", network: "vpn" }).success).toBe(false); }); }); describe("api keys", () => { it("generates prefixed keys and stable hashes", () => { const k = generateApiKey("live"); expect(k.plaintext.startsWith("fch_live_")).toBe(true); expect(k.prefix).toBe(k.plaintext.slice(0, 13)); expect(hashApiKey(k.plaintext)).toBe(k.hash); expect(generateApiKey("test").plaintext.startsWith("fch_test_")).toBe(true); }); }); describe("block detection & text", () => { it("detects challenge pages and captchas", () => { expect(looksBlocked(403, "", {}).blocked).toBe(true); expect(looksBlocked(503, "Just a moment...", {}).blocked).toBe(true); expect(looksBlocked(200, "Security check
Please verify you are human", {}).blocked).toBe(true); expect(looksBlocked(200, "

Contact

", {}).blocked).toBe(false); expect(looksBlocked(200, "Hello", {}).blocked).toBe(false); expect(looksBlocked(200, "", { "cf-mitigated": "challenge" })).toMatchObject({ blocked: true, reason: "cloudflare_challenge", challenge: true }); expect(looksBlocked(403, "Access Denied", { server: "AkamaiGHost", "set-cookie": "_abck=1; Path=/" })).toMatchObject({ blocked: true, vendor: "akamai" }); expect(looksBlocked(429, "", { "retry-after": "3" })).toMatchObject({ blocked: true, reason: "rate_limited", retryAfterMs: 3000 }); expect(looksBlocked(200, "", {})).toMatchObject({ blocked: true, reason: "empty_html" }); expect(looksBlocked(200, "DataDome", {})).toMatchObject({ blocked: true, vendor: "datadome" }); // Vendor beacons on a normal page must not count as blocks. const normal = `Cloudflare Challenge - Example

You bypassed the Cloudflare challenge!

${"content ".repeat(200)}

`; expect(looksBlocked(200, normal, { server: "cloudflare" }).blocked).toBe(false); expect(looksBlocked(403, normal, { server: "cloudflare" })).toMatchObject({ blocked: true, vendor: "cloudflare" }); expect(looksBlocked(200, "Just a moment...
", {})).toMatchObject({ blocked: true, reason: "cloudflare_challenge" }); }); it("extracts readable text", () => { expect(htmlToText("

Hi

There & back

")).toBe("Hi\nThere & back"); }); it("normalizes geography", () => { expect(normalizeGeo({ country: "ca", region: "QC", city: "Québec" })).toEqual({ country: "CA", region: "quebec", city: "quebec" }); expect(normalizeGeo({ country: "US", region: "ny" }).region).toBe("new_york"); }); });