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