Files
rdapper/src/whois/throttle.ts
T
jake 3023c5dd1b feat: add SSRF host validation, rate_limited/unparseable error codes, and referral chain warnings
- Add `isSafeWhoisReferralHost` to reject private/loopback/link-local IPs and malformed hostnames before following WHOIS referrals (SSRF guard)
- Detect WHOIS throttle replies in the referral chain: drop the registrar response, keep the registry record, and surface a warning instead of returning bad data
- Add `looksEmptyWhois` guard in the main lookup path: a "registered" record with no parseable fields now fails with `errorCode: "unparseable"` rather than resolving silently
- Map RDAP 429 responses to a structured `rate_limited` error (including `Retry-After` header) so callers can distinguish throttling from generic HTTP errors and the fallback to WHOIS is recorded in `attempts`
- Add `rate_limited` and `unparseable` to `LookupErrorCode`
- Change `collectWhoisReferralChain` to return `{ results, warnings }` instead of a bare array; warnings are merged onto the final `DomainRecord`
- Remove the `followWhoisReferrals` fallback path from `index.ts` (dead code after the chain API stabilised)
2026-09-19 12:14:19 -04:00

41 lines
1.3 KiB
TypeScript

import type { DomainRecord } from "../types";
// Real WHOIS records are much longer; throttle notices and error pages are short.
const MAX_THROTTLE_TEXT_LENGTH = 2048;
const THROTTLE_PATTERNS: RegExp[] = [
/rate[\s-]?limit/i,
/limit\s+exceeded/i,
/quota\s+exceeded/i,
/exceeded\s+.{0,40}(quer|limit|request|connection)/i,
/too\s+many\s+(quer|request|connection)/i,
/try\s+again\s+(later|in)/i,
/access\s+(denied|limit)/i,
/\b(blocked|blacklisted|banned)\b/i,
/^\s*<(!doctype|html)\b/i,
];
/**
* Heuristic: does this WHOIS response look like a throttle notice or error page rather than
* a domain record? Only short responses qualify, so a real record that mentions "rate limit"
* in a remark is not rejected.
*/
export function detectWhoisThrottle(text: string | undefined): boolean {
if (!text) return false;
if (text.length > MAX_THROTTLE_TEXT_LENGTH) return false;
return THROTTLE_PATTERNS.some((re) => re.test(text));
}
/** True when a normalized WHOIS record carries none of the fields a real registration would. */
export function looksEmptyWhois(record: DomainRecord): boolean {
return !(
record.registrar ||
record.creationDate ||
record.updatedDate ||
record.expirationDate ||
record.nameservers?.length ||
record.statuses?.length ||
record.contacts?.length
);
}