import { describe, expect, it } from "@suaveplan/testing/runner"; import { SubdomainTenantResolverError, TENANT_SUBDOMAIN_CODES, } from "../error-codes/error-codes.js"; import { createSubdomainTenantResolver } from "../resolver/resolver.js"; import { cidrMatches, isTrustedProxy, parseCidr, parseIp, } from "./forwarded.js"; function reqWith( headers: Record, extras: Record = {} ): Request { const req = new Request(`https://example.com/${Math.random()}`, { headers, }); for (const [k, v] of Object.entries(extras)) { Object.defineProperty(req, k, { value: v, enumerable: true }); } return req; } describe("parseCidr", () => { it("parses IPv4 with prefix", { timeout: 5000 }, () => { const c = parseCidr("10.0.0.0/8"); expect(c).not.toBeNull(); expect(c?.family).toBe(4); expect(c?.prefix).toBe(8); expect(Array.from(c?.bytes ?? [])).toEqual([10, 0, 0, 0]); }); it("defaults IPv4 prefix to 32 when omitted", { timeout: 5000 }, () => { const c = parseCidr("10.4.7.2"); expect(c?.prefix).toBe(32); }); it("parses IPv6 with prefix and :: shorthand", { timeout: 5000 }, () => { const c = parseCidr("2001:db8::/32"); expect(c?.family).toBe(6); expect(c?.prefix).toBe(32); }); it("defaults IPv6 prefix to 128 when omitted", { timeout: 5000 }, () => { const c = parseCidr("2001:db8::1"); expect(c?.prefix).toBe(128); }); it("returns null for malformed input", { timeout: 5000 }, () => { expect(parseCidr("")).toBeNull(); expect(parseCidr(null as unknown as string)).toBeNull(); expect(parseCidr("999.0.0.0/8")).toBeNull(); expect(parseCidr("10.0.0.0/33")).toBeNull(); expect(parseCidr("10.0.0.0/abc")).toBeNull(); expect(parseCidr("2001:db8::/129")).toBeNull(); expect(parseCidr("2001:db8::/zz")).toBeNull(); expect(parseCidr("not-an-ip")).toBeNull(); expect(parseCidr("1:2:3:4:5:6:7:8:9")).toBeNull(); expect(parseCidr("g:db8::1")).toBeNull(); expect(parseCidr("1.2.3")).toBeNull(); expect(parseCidr("1.2.3.4.5")).toBeNull(); }); it("rejects extra ipv6 groups without ::", { timeout: 5000 }, () => { expect(parseCidr("1:2:3:4:5:6:7")).toBeNull(); }); it("parses fully-expanded ipv6 (no ::)", { timeout: 5000 }, () => { const c = parseCidr("1:2:3:4:5:6:7:8/128"); expect(c?.family).toBe(6); expect(c?.prefix).toBe(128); }); it("rejects ipv6 hex group longer than 4 chars", { timeout: 5000, }, () => { expect(parseCidr("12345::1")).toBeNull(); }); it("rejects ipv6 empty hex group", { timeout: 5000 }, () => { // Triple colon makes the middle slot an empty group, not a ::. expect(parseCidr("1:::2")).toBeNull(); }); it("parses '::' as all-zero", { timeout: 5000 }, () => { const c = parseCidr("::"); expect(c?.family).toBe(6); expect(Array.from(c?.bytes ?? [])).toEqual(Array(16).fill(0)); }); it("parses '::1'", { timeout: 5000 }, () => { const c = parseCidr("::1"); expect(c?.family).toBe(6); }); it("parses '1::'", { timeout: 5000 }, () => { const c = parseCidr("1::"); expect(c?.family).toBe(6); }); it("strips IPv6 zone-id before parsing", { timeout: 5000 }, () => { const c = parseCidr("fe80::1%eth0/64"); expect(c?.family).toBe(6); expect(c?.prefix).toBe(64); }); }); describe("parseIp", () => { it("parses IPv4 and IPv6", { timeout: 5000 }, () => { expect(parseIp("10.0.0.1")?.family).toBe(4); expect(parseIp("2001:db8::1")?.family).toBe(6); }); it("returns null for empty or malformed", { timeout: 5000 }, () => { expect(parseIp("")).toBeNull(); expect(parseIp(null as unknown as string)).toBeNull(); expect(parseIp("not-an-ip")).toBeNull(); expect(parseIp("zzzz::1")).toBeNull(); }); }); describe("cidrMatches", () => { it("matches v4 inside /8", { timeout: 5000 }, () => { const cidr = parseCidr("10.0.0.0/8"); const ip = parseIp("10.4.7.2"); expect(cidrMatches(ip!, cidr!)).toBe(true); }); it("rejects v4 outside /8", { timeout: 5000 }, () => { const cidr = parseCidr("10.0.0.0/8"); const ip = parseIp("8.8.8.8"); expect(cidrMatches(ip!, cidr!)).toBe(false); }); it("matches /0 universally inside family", { timeout: 5000 }, () => { const cidr = parseCidr("0.0.0.0/0"); expect(cidrMatches(parseIp("1.2.3.4")!, cidr!)).toBe(true); }); it("matches v6 inside /32", { timeout: 5000 }, () => { const cidr = parseCidr("2001:db8::/32"); expect(cidrMatches(parseIp("2001:db8:1::1")!, cidr!)).toBe(true); expect(cidrMatches(parseIp("2001:dead::1")!, cidr!)).toBe(false); }); it("family-disjoint match returns false", { timeout: 5000 }, () => { const cidr = parseCidr("10.0.0.0/8"); const ip = parseIp("2001:db8::1"); expect(cidrMatches(ip!, cidr!)).toBe(false); }); it("handles partial-byte prefixes", { timeout: 5000 }, () => { const cidr = parseCidr("10.4.0.0/14"); expect(cidrMatches(parseIp("10.4.0.1")!, cidr!)).toBe(true); expect(cidrMatches(parseIp("10.7.255.255")!, cidr!)).toBe(true); expect(cidrMatches(parseIp("10.8.0.0")!, cidr!)).toBe(false); }); }); describe("isTrustedProxy", () => { it("returns false for nullish remoteAddress", { timeout: 5000 }, () => { expect(isTrustedProxy(null, [])).toBe(false); expect(isTrustedProxy(undefined, [])).toBe(false); }); it("returns false for malformed remoteAddress", { timeout: 5000 }, () => { const c = parseCidr("10.0.0.0/8"); expect(isTrustedProxy("not-an-ip", [c!])).toBe(false); }); it("returns true when any CIDR matches", { timeout: 5000 }, () => { const cidrs = [parseCidr("10.0.0.0/8")!, parseCidr("172.16.0.0/12")!]; expect(isTrustedProxy("172.16.1.1", cidrs)).toBe(true); }); it("returns false when no CIDR matches", { timeout: 5000 }, () => { const cidrs = [parseCidr("10.0.0.0/8")!]; expect(isTrustedProxy("8.8.8.8", cidrs)).toBe(false); }); }); describe("resolver — X-Forwarded-Host trust", () => { it("trusted proxy honours forwarded host", { timeout: 5000 }, async () => { const r = createSubdomainTenantResolver({ baseDomain: "api.example.com", trustForwardedHost: true, trustedProxyCidrs: ["10.0.0.0/8"], }); const req = reqWith( { host: "internal.lb", "x-forwarded-host": "acme.api.example.com", }, { remoteAddress: "10.4.7.2" } ); const ctx = await r.resolve(req); expect(ctx).toEqual({ id: "acme", slug: "acme" }); }); it("untrusted proxy raises FORWARDED_HOST_UNTRUSTED in strict mode", { timeout: 5000, }, async () => { const r = createSubdomainTenantResolver({ baseDomain: "api.example.com", strict: true, trustForwardedHost: true, trustedProxyCidrs: ["10.0.0.0/8"], }); const req = reqWith( { host: "acme.api.example.com", "x-forwarded-host": "victim.api.example.com", }, { remoteAddress: "8.8.8.8" } ); let thrown: unknown; try { await r.resolve(req); } catch (e) { thrown = e; } expect(thrown).toBeInstanceOf(SubdomainTenantResolverError); expect((thrown as SubdomainTenantResolverError).code).toBe( TENANT_SUBDOMAIN_CODES.FORWARDED_HOST_UNTRUSTED ); }); it("non-strict: untrusted proxy returns null", { timeout: 5000, }, async () => { const r = createSubdomainTenantResolver({ baseDomain: "api.example.com", trustForwardedHost: true, trustedProxyCidrs: ["10.0.0.0/8"], }); const req = reqWith( { host: "acme.api.example.com", "x-forwarded-host": "victim.api.example.com", }, { remoteAddress: "8.8.8.8" } ); expect(await r.resolve(req)).toBeNull(); }); it("trustForwardedHost=false silently drops the header", { timeout: 5000, }, async () => { const r = createSubdomainTenantResolver({ baseDomain: "api.example.com", strict: true, }); const req = reqWith( { host: "acme.api.example.com", "x-forwarded-host": "victim.api.example.com", }, { remoteAddress: "10.4.7.2" } ); const ctx = await r.resolve(req); expect(ctx).toEqual({ id: "acme", slug: "acme" }); }); });