From 5bc6debe3042e89255a6cc88e509ae9a956d2134 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 26 Sep 2025 11:48:02 -0400 Subject: [PATCH] Enhance WHOIS fallback error handling and add IANA WHOIS parsing utilities Updated the error messages in the lookupDomain function to provide clearer guidance when RDAP is unavailable or when no WHOIS server is discovered for a TLD. Introduced new utility functions to parse IANA WHOIS responses, improving the discovery process for WHOIS servers and registration information URLs. --- renovate.json | 4 +-- src/index.ts | 17 ++++++++++-- src/whois/discovery.ts | 62 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/renovate.json b/renovate.json index 5db72dd..22a9943 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,4 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ] + "extends": ["config:recommended"] } diff --git a/src/index.ts b/src/index.ts index aa20ebc..517587d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,8 @@ import { whoisQuery } from "./whois/client.js"; import { extractWhoisReferral, ianaWhoisServerForTld, + getIanaWhoisTextForTld, + parseIanaRegistrationInfoUrl, } from "./whois/discovery.js"; import { normalizeWhois } from "./whois/normalize.js"; import { WHOIS_TLD_EXCEPTIONS } from "./whois/servers.js"; @@ -54,7 +56,7 @@ export async function lookupDomain( if (opts?.rdapOnly) { return { ok: false, - error: "RDAP not available or failed for this TLD", + error: `RDAP not available or failed for TLD '${tld}'. Many TLDs do not publish RDAP; try WHOIS fallback (omit rdapOnly).`, }; } } @@ -62,7 +64,18 @@ export async function lookupDomain( // WHOIS fallback path const whoisServer = await ianaWhoisServerForTld(tld, opts); if (!whoisServer) { - return { ok: false, error: "No WHOIS server discovered for TLD" }; + // Provide a clearer, actionable message + const ianaText = await getIanaWhoisTextForTld(tld, opts); + const regUrl = ianaText + ? parseIanaRegistrationInfoUrl(ianaText) + : undefined; + const hint = regUrl + ? ` See registration info at ${regUrl}.` + : ""; + return { + ok: false, + error: `No WHOIS server discovered for TLD '${tld}'. This registry may not publish public WHOIS over port 43.${hint}`, + }; } // Query the TLD server first; if it returns a referral, we follow it below. let res = await whoisQuery(whoisServer, domain, opts); diff --git a/src/whois/discovery.ts b/src/whois/discovery.ts index 1d46eb0..f0dba6c 100644 --- a/src/whois/discovery.ts +++ b/src/whois/discovery.ts @@ -2,6 +2,62 @@ import type { LookupOptions } from "../types.js"; import { whoisQuery } from "./client.js"; import { WHOIS_TLD_EXCEPTIONS } from "./servers.js"; +/** + * Parse the IANA WHOIS response for a TLD and extract the WHOIS server + * without crossing line boundaries. Some TLDs (e.g. .np) leave the field + * blank, in which case this returns undefined. + */ +export function parseIanaWhoisServer(text: string): string | undefined { + // Search lines in priority order: whois, refer, whois server + const fields = ["whois", "refer", "whois server"]; + const lines = String(text).split(/\r?\n/); + for (const field of fields) { + for (const raw of lines) { + const line = raw.trimEnd(); + // Match beginning of line, allowing leading spaces, case-insensitive + const re = new RegExp(`^\\s*${field}\\s*:\\s*(.*?)$`, "i"); + const m = line.match(re); + if (m) { + const value = (m[1] || "").trim(); + if (value) return value; + } + } + } + return undefined; +} + +/** + * Parse a likely registration information URL from an IANA WHOIS response. + * Looks at lines like: + * remarks: Registration information: http://example.tld + * url: https://registry.example + */ +export function parseIanaRegistrationInfoUrl( + text: string, +): string | undefined { + const lines = String(text).split(/\r?\n/); + for (const raw of lines) { + const line = raw.trim(); + if (!/^\s*(remarks|url|website)\s*:/i.test(line)) continue; + const urlMatch = line.match(/https?:\/\/\S+/i); + if (urlMatch?.[0]) return urlMatch[0]; + } + return undefined; +} + +/** Fetch raw IANA WHOIS text for a TLD (best-effort). */ +export async function getIanaWhoisTextForTld( + tld: string, + options?: LookupOptions, +): Promise { + try { + const res = await whoisQuery("whois.iana.org", tld.toLowerCase(), options); + return res.text; + } catch { + return undefined; + } +} + /** * Best-effort discovery of the authoritative WHOIS server for a TLD via IANA root DB. */ @@ -18,11 +74,7 @@ export async function ianaWhoisServerForTld( try { const res = await whoisQuery("whois.iana.org", key, options); const txt = res.text; - const m = - txt.match(/^whois:\s*(\S+)/im) || - txt.match(/^refer:\s*(\S+)/im) || - txt.match(/^whois server:\s*(\S+)/im); - const server = m?.[1]; + const server = parseIanaWhoisServer(txt); if (server) return normalizeServer(server); } catch { // fallthrough to exceptions/guess