const { fetchJSON } = require("./services/http-helpers"); const { whoisCacheGet, whoisCacheSet, WHOIS_NOT_FOUND_CACHE_TTL } = require("./caches/response-caches"); // Feature 27: WHOIS via RIPE DB async function fetchWhois(resource) { const result = { resource, type: null, data: null, error: null }; try { const trimmed = resource.trim(); if (/^(AS)?\d+$/i.test(trimmed)) { result.type = "aut-num"; const asn = trimmed.replace(/^AS/i, ""); // Check cache first const cached = whoisCacheGet(asn); if (cached !== undefined) { result.data = cached; if (!cached) result.error = "Not found in any RIR database (cached)"; return result; } // Try RIPE first const ripeData = await fetchJSON("https://rest.db.ripe.net/search.json?query-string=AS" + asn + "&type-filter=aut-num&source=ripe", { timeout: 5000 }).catch(() => null); if (ripeData && ripeData.objects && ripeData.objects.object) { const obj = ripeData.objects.object[0]; const attrs = obj.attributes?.attribute || []; const parsed = {}; attrs.forEach((a) => { if (!parsed[a.name]) parsed[a.name] = []; parsed[a.name].push(a.value); }); result.data = { aut_num: (parsed["aut-num"] || [])[0] || "", as_name: (parsed["as-name"] || [])[0] || "", descr: parsed["descr"] || [], org: (parsed["org"] || [])[0] || "", admin_c: parsed["admin-c"] || [], tech_c: parsed["tech-c"] || [], mnt_by: parsed["mnt-by"] || [], status: (parsed["status"] || [])[0] || "", created: (parsed["created"] || [])[0] || "", last_modified: (parsed["last-modified"] || [])[0] || "", source: (parsed["source"] || [])[0] || "", import: parsed["import"] || [], export: parsed["export"] || [], remarks: parsed["remarks"] || [], }; whoisCacheSet(asn, result.data); } // If RIPE didn't find it, try all other RIRs via RDAP in parallel (3s timeout) if (!result.data) { const rdapEndpoints = [ { name: "APNIC", url: "https://rdap.apnic.net/autnum/" + asn }, { name: "ARIN", url: "https://rdap.arin.net/registry/autnum/" + asn }, { name: "LACNIC", url: "https://rdap.lacnic.net/rdap/autnum/" + asn }, { name: "AFRINIC", url: "https://rdap.afrinic.net/rdap/autnum/" + asn }, ]; const rdapResults = await Promise.all(rdapEndpoints.map((ep) => fetchJSON(ep.url, { timeout: 3000 }).then((d) => { if (!d || d.errorCode || !d.handle) return null; return { source: ep.name, data: d }; }).catch(() => null) )); const found = rdapResults.find((r) => r !== null); if (found) { const d = found.data; const remarks = (d.remarks || []).map((r) => (r.description || []).join(" ")); const entities = d.entities || []; const adminContacts = entities.filter((e) => (e.roles || []).includes("administrative")).map((e) => e.handle || ""); const techContacts = entities.filter((e) => (e.roles || []).includes("technical")).map((e) => e.handle || ""); const events = d.events || []; const created = (events.find((e) => e.eventAction === "registration") || {}).eventDate || ""; const lastMod = (events.find((e) => e.eventAction === "last changed") || {}).eventDate || ""; result.data = { aut_num: "AS" + asn, as_name: d.name || "", descr: remarks, org: (entities.find((e) => (e.roles || []).includes("registrant")) || {}).handle || "", admin_c: adminContacts, tech_c: techContacts, mnt_by: [], status: (d.status || []).join(", "), created: created, last_modified: lastMod, source: found.source + " (RDAP)", import: [], export: [], remarks: remarks, }; whoisCacheSet(asn, result.data); } else { result.error = "Not found in any RIR database (RIPE, APNIC, ARIN, LACNIC, AFRINIC)"; // Short TTL: this "not found" likely reflects a transient failure across // all 5 sources, not a confirmed non-existent ASN -- see WHOIS_NOT_FOUND_CACHE_TTL. whoisCacheSet(asn, null, WHOIS_NOT_FOUND_CACHE_TTL); } } } else if (/[\/:]/.test(trimmed) || /^\d+\.\d+\.\d+/.test(trimmed)) { result.type = "inetnum"; const ripeData = await fetchJSON("https://rest.db.ripe.net/search.json?query-string=" + encodeURIComponent(trimmed) + "&type-filter=inetnum,inet6num"); if (ripeData && ripeData.objects && ripeData.objects.object) { const results = ripeData.objects.object.map((obj) => { const attrs = obj.attributes?.attribute || []; const parsed = {}; attrs.forEach((a) => { if (!parsed[a.name]) parsed[a.name] = []; parsed[a.name].push(a.value); }); return { inetnum: (parsed["inetnum"] || parsed["inet6num"] || [])[0] || "", netname: (parsed["netname"] || [])[0] || "", descr: parsed["descr"] || [], country: (parsed["country"] || [])[0] || "", org: (parsed["org"] || [])[0] || "", admin_c: parsed["admin-c"] || [], tech_c: parsed["tech-c"] || [], mnt_by: parsed["mnt-by"] || [], status: (parsed["status"] || [])[0] || "", created: (parsed["created"] || [])[0] || "", last_modified: (parsed["last-modified"] || [])[0] || "", source: (parsed["source"] || [])[0] || "", }; }); result.data = results.length === 1 ? results[0] : results; } else { result.error = "Not found in RIPE DB"; } } else { result.type = "domain"; const ripeData = await fetchJSON("https://rest.db.ripe.net/search.json?query-string=" + encodeURIComponent(trimmed) + "&type-filter=domain"); if (ripeData && ripeData.objects && ripeData.objects.object) { const obj = ripeData.objects.object[0]; const attrs = obj.attributes?.attribute || []; const parsed = {}; attrs.forEach((a) => { if (!parsed[a.name]) parsed[a.name] = []; parsed[a.name].push(a.value); }); result.data = { domain: (parsed["domain"] || [])[0] || "", descr: parsed["descr"] || [], admin_c: parsed["admin-c"] || [], tech_c: parsed["tech-c"] || [], zone_c: parsed["zone-c"] || [], nserver: parsed["nserver"] || [], mnt_by: parsed["mnt-by"] || [], created: (parsed["created"] || [])[0] || "", last_modified: (parsed["last-modified"] || [])[0] || "", source: (parsed["source"] || [])[0] || "", }; } else { result.error = "Not found in RIPE DB"; } } } catch (err) { result.error = err.message; } return result; } module.exports = { fetchWhois };