Files
rdapper/src/whois/referral.test.ts
T
jake 35959b208b feat: surface retryAfterMs, add blocked error code, and harden private-address guard
- Parse `Retry-After` headers (delay-seconds or HTTP-date) into `retryAfterMs` via a new `parseRetryAfterMs` helper and expose the value on both `LookupAttempt` and `LookupResult` so callers can honour server-requested back-off without parsing the error string
- Propagate `retryAfterMs` through `classifyError`, `traced`, `failure`, and the `rdap_unavailable` path so the value surfaces on the top-level result
- Add `blocked` to `LookupErrorCode` for WHOIS servers that permanently refuse a client (distinct from `rate_limited`, which is a temporary throttle that may succeed on retry)
- Add `blockPrivateAddresses` to `WhoisTransportOptions`; when set, a custom `dns.lookup` shim rejects the connection before it opens if any resolved address is non-public, covering DNS rebinding and hostnames that resolve to private ranges
- Expand `isPrivateIp` / `isSafeWhoisReferralHost` to reject IPv4-mapped (`::ffff:7f00:1`), NAT64 (`64:ff9b::`), 6to4 (`2002:7f00::`), Teredo (`2001:0:`), deprecated site-local (`fec0::`), and bracketed IPv6 literals
2026-09-19 12:25:30 -04:00

68 lines
2.7 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
vi.mock("./client.js", () => ({
whoisQuery: vi.fn(async (server: string) => {
if (server === "whois.nic.io") {
// TLD WHOIS shows a clearly registered domain and a registrar referral
return {
serverQueried: server,
text: `Domain Name: RAINDROP.IO\nCreation Date: 2013-08-20T20:30:16Z\nRegistry Expiry Date: 2027-08-20T20:30:16Z\nRegistrar WHOIS Server: whois.1api.net\nName Server: BEAU.NS.CLOUDFLARE.COM\nName Server: BARBARA.NS.CLOUDFLARE.COM\n`,
};
}
// Registrar WHOIS contradicts with an availability phrase
return {
serverQueried: server,
text: "No match for RAINDROP.IO",
};
}),
}));
import { collectWhoisReferralChain } from "./referral";
describe("WHOIS referral contradiction handling", () => {
it("collects chain and does not append contradictory registrar", async () => {
const { results: chain } = await collectWhoisReferralChain("whois.nic.io", "raindrop.io", {
followWhoisReferral: true,
maxWhoisReferralHops: 2,
});
expect(Array.isArray(chain)).toBe(true);
// Mocked registrar is contradictory, so chain should contain only the TLD response
expect(chain.length).toBe(1);
expect(chain[0]?.serverQueried).toBe("whois.nic.io");
});
});
describe("WHOIS referral safety", () => {
it("does not query an unsafe referral host and reports a warning", async () => {
const { whoisQuery } = await import("./client.js");
const mocked = vi.mocked(whoisQuery);
mocked.mockClear();
mocked.mockImplementation(async (server: string) => ({
serverQueried: server,
text: "Domain Name: EVIL.COM\nCreation Date: 2013-08-20T20:30:16Z\nRegistrar WHOIS Server: 169.254.169.254\n",
}));
const { results, warnings } = await collectWhoisReferralChain("whois.nic.io", "evil.com", {
followWhoisReferral: true,
});
expect(results).toHaveLength(1);
expect(mocked).toHaveBeenCalledTimes(1);
expect(warnings[0]).toMatch(/unsafe host/);
});
it("keeps the registry record when the registrar throttles", async () => {
const { whoisQuery } = await import("./client.js");
vi.mocked(whoisQuery).mockImplementation(async (server: string) => ({
serverQueried: server,
text:
server === "whois.nic.io"
? "Domain Name: X.IO\nCreation Date: 2013-08-20T20:30:16Z\nRegistrar WHOIS Server: whois.1api.net\n"
: "WHOIS LIMIT EXCEEDED",
}));
const { results, warnings } = await collectWhoisReferralChain("whois.nic.io", "x.io", {
followWhoisReferral: true,
});
expect(results).toHaveLength(1);
expect(warnings[0]).toMatch(/rate limited/);
});
});