mirror of
https://github.com/jakejarvis/rdapper.git
synced 2026-09-23 03:45:31 -04:00
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
This commit is contained in:
+12
-54
@@ -7,53 +7,7 @@ import { whoisQuery } from "./client";
|
||||
import { extractWhoisReferral } from "./discovery";
|
||||
import { isSafeWhoisReferralHost } from "./host";
|
||||
import { isAvailableByWhois, normalizeWhois } from "./normalize";
|
||||
import { detectWhoisThrottle, looksEmptyWhois } from "./throttle";
|
||||
|
||||
/**
|
||||
* Follow registrar WHOIS referrals up to a configured hop limit.
|
||||
* Returns the last successful WHOIS response (best-effort; keeps original on failures).
|
||||
*/
|
||||
export async function followWhoisReferrals(
|
||||
initialServer: string,
|
||||
domain: string,
|
||||
opts?: LookupOptions,
|
||||
ctx?: LookupContext,
|
||||
): Promise<WhoisQueryResult> {
|
||||
const maxHops = Math.max(0, opts?.maxWhoisReferralHops ?? 2);
|
||||
// First query against the provided server
|
||||
let current = await tracedWhoisQuery(initialServer, domain, opts, ctx);
|
||||
if (opts?.followWhoisReferral === false || maxHops === 0) return current;
|
||||
|
||||
const visited = new Set<string>([normalize(current.serverQueried)]);
|
||||
let hops = 0;
|
||||
// Iterate while we see a new referral and are under hop limit
|
||||
while (hops < maxHops) {
|
||||
throwIfAborted(opts?.signal);
|
||||
const next = extractWhoisReferral(current.text);
|
||||
if (!next) break;
|
||||
if (!isSafeWhoisReferralHost(normalize(next))) break;
|
||||
const normalized = normalize(next);
|
||||
if (visited.has(normalized)) break; // cycle protection / same as current
|
||||
visited.add(normalized);
|
||||
try {
|
||||
const res = await tracedWhoisQuery(next, domain, opts, ctx);
|
||||
// Prefer authoritative TLD response when registrar contradicts availability
|
||||
const registeredBefore = !isAvailableByWhois(current.text);
|
||||
const registeredAfter = !isAvailableByWhois(res.text);
|
||||
if (registeredBefore && !registeredAfter) {
|
||||
// Registrar claims availability but TLD shows registered: keep TLD
|
||||
break;
|
||||
}
|
||||
current = res; // adopt registrar when it does not downgrade registration
|
||||
} catch {
|
||||
throwIfAborted(opts?.signal);
|
||||
// If referral server fails, stop following and keep the last good response
|
||||
break;
|
||||
}
|
||||
hops += 1;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
import { detectWhoisRefusal, looksEmptyWhois } from "./throttle";
|
||||
|
||||
/**
|
||||
* Collect the WHOIS referral chain starting from the TLD server.
|
||||
@@ -88,7 +42,7 @@ export async function collectWhoisReferralChain(
|
||||
if (visited.has(normalized)) break;
|
||||
visited.add(normalized);
|
||||
try {
|
||||
const res = await tracedWhoisQuery(next, domain, opts, ctx);
|
||||
const res = await tracedWhoisQuery(next, domain, opts, ctx, true);
|
||||
// If registrar claims availability while TLD indicated registered, stop.
|
||||
const registeredBefore = !isAvailableByWhois(current.text);
|
||||
const registeredAfter = !isAvailableByWhois(res.text);
|
||||
@@ -109,8 +63,8 @@ export async function collectWhoisReferralChain(
|
||||
throwIfAborted(opts?.signal);
|
||||
const { code, error } = classifyError(err);
|
||||
warnings.push(
|
||||
code === "rate_limited"
|
||||
? `WHOIS referral ${normalized} rate limited the query`
|
||||
code === "rate_limited" || code === "blocked"
|
||||
? `WHOIS referral ${normalized} ${code === "blocked" ? "blocked" : "rate limited"} the query`
|
||||
: `WHOIS referral ${normalized} failed (${error})`,
|
||||
);
|
||||
break;
|
||||
@@ -130,17 +84,21 @@ function tracedWhoisQuery(
|
||||
domain: string,
|
||||
opts?: LookupOptions,
|
||||
ctx?: LookupContext,
|
||||
referral = false,
|
||||
): Promise<WhoisQueryResult> {
|
||||
return traced(
|
||||
ctx,
|
||||
{ phase: "whois", server: server.replace(/^whois:\/\//i, "") },
|
||||
async (notes) => {
|
||||
const res = await whoisQuery(server, domain, opts);
|
||||
const res = await whoisQuery(server, domain, opts, { blockPrivateAddresses: referral });
|
||||
if (res.partial) notes.partial = true;
|
||||
if (detectWhoisThrottle(res.text)) {
|
||||
const refusal = detectWhoisRefusal(res.text);
|
||||
if (refusal) {
|
||||
throw new RdapperError(
|
||||
"rate_limited",
|
||||
`WHOIS server ${res.serverQueried} rate limited the query`,
|
||||
refusal,
|
||||
refusal === "blocked"
|
||||
? `WHOIS server ${res.serverQueried} refuses requests from this client`
|
||||
: `WHOIS server ${res.serverQueried} rate limited the query`,
|
||||
{ stage: "read" },
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user