From 9012d2931f1d174880303dacb8925d0ff3e854bb Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Thu, 2 Apr 2026 23:08:54 +0000 Subject: [PATCH] fix: RIR+Country empty (RIPE Stat .location field), RDAP parallel race (v0.6.7) --- server.js | 60 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/server.js b/server.js index aa8fc4e..4a67a78 100644 --- a/server.js +++ b/server.js @@ -2089,7 +2089,7 @@ const server = http.createServer(async (req, res) => { JSON.stringify({ status, service: "PeerCortex", - version: "0.6.6", + version: "0.6.7", timestamp: new Date().toISOString(), uptime_seconds: Math.floor(process.uptime()), memory_mb: Math.round(mem.heapUsed / 1024 / 1024), @@ -3138,10 +3138,13 @@ const server = http.createServer(async (req, res) => { } const pocQuery = netId ? "/poc?net_id=" + netId + "&limit=25" : null; + // RDAP: try all 5 RIRs in parallel, take the first valid response (fast race) const rdapForReg = [ "https://rdap.db.ripe.net/autnum/" + asn, - "https://rdap.apnic.net/autnum/" + asn, "https://rdap.arin.net/registry/autnum/" + asn, + "https://rdap.apnic.net/autnum/" + asn, + "https://rdap.lacnic.net/rdap/autnum/" + asn, + "https://rdap.afrinic.net/rdap/autnum/" + asn, ]; const promises = [ @@ -3156,15 +3159,15 @@ const server = http.createServer(async (req, res) => { timedFetch("PeeringDB IXLan", cachedIxlan ? Promise.resolve(cachedIxlan) : fetchPeeringDBWithRetry(ixQuery)), timedFetch("PeeringDB Facilities", cachedFac ? Promise.resolve(cachedFac) : (netId ? fetchPeeringDBWithRetry("/netfac?net_id=" + netId + "&limit=1000") : Promise.resolve(null))), timedFetch("PeeringDB Contacts", pocQuery ? fetchPeeringDB(pocQuery).catch(() => null) : Promise.resolve(null)), - timedFetch("RDAP Registration", (async () => { - for (const url of rdapForReg) { - try { - const d = await fetchJSON(url, { timeout: 5000 }); - if (d && !d.errorCode && d.handle) return d; - } catch(e) {} - } - return null; - })()), + timedFetch("RDAP Registration", Promise.race([ + // All 5 RIR RDAP endpoints in parallel — first valid wins + ...rdapForReg.map(url => + fetchJSON(url, { timeout: 4000 }) + .then(d => (d && !d.errorCode && d.handle) ? d : new Promise(() => {})) + .catch(() => new Promise(() => {})) + ), + new Promise(resolve => setTimeout(() => resolve(null), 5000)), + ])), ]; const [prefixData, neighbourData, overviewData, rirData, atlasProbeData, bgpHeData, visibilityData, prefixSizeData, ixlanData, facData, pocData, rdapData] = await Promise.all(promises); @@ -3308,14 +3311,45 @@ const server = http.createServer(async (req, res) => { let rir = ""; let country = ""; + // RIPE Stat rir-stats-country uses 'location' field (not 'country' or 'rir') if (Array.isArray(rirEntries) && rirEntries.length > 0) { + country = rirEntries[0]?.location || rirEntries[0]?.country || ""; rir = rirEntries[0]?.rir || ""; - country = rirEntries[0]?.country || ""; } if (!rir && rirData?.data) { const rirField = rirData.data.rirs || []; if (rirField.length > 0) rir = rirField[0]?.rir || ""; } + // Derive RIR from rdapData.port43 (e.g. "whois.ripe.net" → "RIPE") + if (!rir && rdapData && rdapData.port43) { + const p43 = (rdapData.port43 || "").toLowerCase(); + if (p43.includes("ripe")) rir = "RIPE"; + else if (p43.includes("arin")) rir = "ARIN"; + else if (p43.includes("apnic")) rir = "APNIC"; + else if (p43.includes("lacnic")) rir = "LACNIC"; + else if (p43.includes("afrinic")) rir = "AFRINIC"; + } + // Also derive RIR from RDAP links (URL of the RDAP endpoint that responded) + if (!rir && rdapData && rdapData.links) { + const selfLink = (rdapData.links.find(l => l.rel === "self") || {}).href || ""; + if (selfLink.includes("ripe")) rir = "RIPE"; + else if (selfLink.includes("arin")) rir = "ARIN"; + else if (selfLink.includes("apnic")) rir = "APNIC"; + else if (selfLink.includes("lacnic")) rir = "LACNIC"; + else if (selfLink.includes("afrinic")) rir = "AFRINIC"; + } + // Last resort: derive RIR from country code (common assignments) + if (!rir && country) { + const ARIN_CC = new Set(["US","CA","AI","AG","BS","BB","BZ","VG","KY","DM","DO","GD","GP","HT","JM","MQ","MS","PR","KN","LC","VC","TT","TC","VI","UM"]); + const APNIC_CC = new Set(["AU","NZ","JP","CN","KR","IN","HK","SG","TW","VN","TH","ID","MY","PK","BD","LK","NP","PH","AF","KH","LA","MM","MN","BT","BN","FJ","PG","WS","TO","VU","SB","KI","NR","TV","FM","MH","PW","CK","NU","TK","WF","PF","NC","GU","MP","AS","CC","CX","HM","NF"]); + const LACNIC_CC = new Set(["BR","AR","MX","CO","CL","PE","VE","EC","UY","BO","PY","CU","GT","HN","SV","NI","CR","PA","GY","SR","GF","AW","CW","SX","BQ","AN"]); + const AFRINIC_CC = new Set(["ZA","NG","KE","EG","GH","TZ","UG","MA","CI","SN","ZM","ZW","AO","MZ","CM","ET","SD","MG","DZ","TN","LY","RW","NA","BW","MW","ML","BF","NE","GN","TD","SO","LS","SZ","ER","DJ","GM","SL","LR","TG","BJ","GW","CF","CG","CD","GQ","ST","KM","MR","SC","MU","RE","CV","BU","SS","EH"]); + if (ARIN_CC.has(country)) rir = "ARIN"; + else if (APNIC_CC.has(country)) rir = "APNIC"; + else if (LACNIC_CC.has(country)) rir = "LACNIC"; + else if (AFRINIC_CC.has(country)) rir = "AFRINIC"; + else rir = "RIPE"; // Europe + rest = RIPE NCC + } const duration = Date.now() - start; @@ -3501,7 +3535,7 @@ const server = http.createServer(async (req, res) => { const result = { meta: { service: "PeerCortex", - version: "0.6.6", + version: "0.6.7", query: "AS" + asn, duration_ms: duration, sources: ["PeeringDB", "RIPE Stat", "bgp.he.net", "Cloudflare RPKI", "RIPE RPKI Validator", "Route Views"],