UNPKG

domainlooker

Version:

A fast CLI and MCP server for inspecting domains: WHOIS/RDAP, DNS, SSL, ports, subdomains, with CSV/JSON export.

162 lines 7.11 kB
import axios from 'axios'; const RDAP_TIMEOUT_MS = 3500; const WHOIS_TIMEOUT_MS = 6000; // `whois` is loaded lazily — only the rare legacy fallback needs it (RDAP handles // most lookups). It ships as CommonJS in older releases (its `lookup` lives on the // default export) and as ESM named exports in newer ones (2.16+), so resolve // `lookup` from whichever shape is present. Loading it on demand also keeps the // ESM build off the module-load path of test runners that can't require it. async function loadLegacyWhoisLookup() { const mod = await import('whois'); const lookup = mod.lookup ?? mod.default?.lookup ?? mod.default; if (typeof lookup !== 'function') { throw new Error('whois: could not resolve a lookup function from the module'); } return lookup; } /** RDAP that lacks these carries too little to prefer over a port-43 record. */ export function hasKeyRegistrationFields(whois) { return !!(whois.registrar || whois.registrationDate || whois.expirationDate); } /** * Registration lookups. RDAP (HTTP/JSON) is tried first because it is fast and * already structured; the legacy port-43 WHOIS protocol is used as a fallback * for registries whose RDAP is missing, sparse, or redacted. */ export class WhoisService { async lookup(domain, options = {}) { const rdap = await this.rdapLookup(domain, options.rdapTimeout ?? RDAP_TIMEOUT_MS).catch(() => null); // Only short-circuit on RDAP when it actually carries the registration // details; a sparse/redacted RDAP record shouldn't hide a richer WHOIS one. if (rdap && hasKeyRegistrationFields(rdap)) { return rdap; } try { const legacy = await this.legacyLookup(domain, options.timeout ?? WHOIS_TIMEOUT_MS); if (Object.keys(legacy).length > 0) { return legacy; } } catch { // Legacy WHOIS failed; fall through to whatever partial RDAP we have. } // Better a partial RDAP record (e.g. just nameservers/status) than nothing. return rdap ?? {}; } async rdapLookup(domain, timeoutMs) { // rdap.org bootstraps to the authoritative RDAP server via redirect. const response = await axios.get(`https://rdap.org/domain/${encodeURIComponent(domain)}`, { timeout: timeoutMs, headers: { Accept: 'application/rdap+json', 'User-Agent': 'domainlooker' }, }); return parseRdapDomain(response.data); } async legacyLookup(domain, timeout) { const lookup = await loadLegacyWhoisLookup(); return new Promise((resolve, reject) => { lookup(domain, { timeout }, (err, data) => { if (err) { reject(err); return; } resolve(parseWhoisText(data)); }); }); } } /** Parse an RDAP domain object into our WhoisData shape. Defensive against partial/redacted records. */ export function parseRdapDomain(data) { const result = {}; if (!data || typeof data !== 'object') return result; const entities = Array.isArray(data.entities) ? data.entities : []; const registrar = entities.find(e => Array.isArray(e.roles) && e.roles.includes('registrar')); if (registrar) { const name = vcardValue(registrar, 'fn'); if (name) result.registrar = name; } const registrant = entities.find(e => Array.isArray(e.roles) && e.roles.includes('registrant')); if (registrant) { const country = vcardCountry(registrant); if (country) result.registrantCountry = country; } const events = Array.isArray(data.events) ? data.events : []; const registration = events.find(e => e.eventAction === 'registration'); const expiration = events.find(e => e.eventAction === 'expiration'); if (registration?.eventDate) result.registrationDate = registration.eventDate; if (expiration?.eventDate) result.expirationDate = expiration.eventDate; const nameServers = (Array.isArray(data.nameservers) ? data.nameservers : []) .map((ns) => ns?.ldhName) .filter((n) => typeof n === 'string'); if (nameServers.length) result.nameServers = nameServers; if (Array.isArray(data.status) && data.status.length) { const status = data.status.filter((s) => typeof s === 'string'); if (status.length) result.status = status; } return result; } /** * Parse raw port-43 WHOIS text. Matches the field label against the KEY (the * text before the first colon) rather than the whole line, so disclaimer prose * like "NOTICE: The expiration date displayed ..." is not mistaken for a value, * and "Registrar WHOIS Server:" is not mistaken for the registrar. */ export function parseWhoisText(data) { const result = {}; for (const line of data.split('\n')) { const idx = line.indexOf(':'); if (idx <= 0) continue; const key = line.slice(0, idx).trim().toLowerCase(); const value = line.slice(idx + 1).trim(); if (!value) continue; if (key === 'registrar' || key === 'sponsoring registrar') { result.registrar = value; } else if (key === 'creation date' || key === 'created' || key === 'created on' || key === 'registered on' || key === 'registration date') { result.registrationDate ?? (result.registrationDate = value); } else if (/expir/.test(key) && /date/.test(key)) { result.expirationDate ?? (result.expirationDate = value); } else if (key === 'name server' || key === 'nserver' || key === 'name servers') { (result.nameServers ?? (result.nameServers = [])); if (!result.nameServers.includes(value)) result.nameServers.push(value); } else if (key === 'registrant country') { result.registrantCountry = value; } else if (key === 'domain status' || key === 'status') { (result.status ?? (result.status = [])); if (!result.status.includes(value)) result.status.push(value); } } return result; } /** Read a value from an RDAP jCard, e.g. `fn` (formatted name). */ function vcardValue(entity, key) { const properties = entity?.vcardArray?.[1]; if (!Array.isArray(properties)) return undefined; const property = properties.find((p) => Array.isArray(p) && p[0] === key); return property && property[3] != null ? String(property[3]) : undefined; } /** The country is the last component of a jCard `adr` structured value. */ function vcardCountry(entity) { const properties = entity?.vcardArray?.[1]; if (!Array.isArray(properties)) return undefined; const adr = properties.find((p) => Array.isArray(p) && p[0] === 'adr'); const value = adr?.[3]; return Array.isArray(value) && value[6] ? String(value[6]) : undefined; } //# sourceMappingURL=whois.js.map