const { fetchRipeStatCached } = require("../services/ripe-stat"); const { fetchBgproutesVisibility } = require("../services/http-helpers"); const { validateRPKIWithCache } = require("../services/rpki"); const BGPROUTES_API_KEY = process.env.BGPROUTES_API_KEY || ""; // Prefix Detail endpoint: /api/prefix/detail?prefix=X async function handlePrefixDetail(req, res, url) { const prefix = url.searchParams.get("prefix") || ""; if (!prefix) { res.writeHead(400); return res.end(JSON.stringify({ error: "Missing prefix parameter" })); } const start = Date.now(); try { const [routingStatus, visibility] = await Promise.all([ fetchRipeStatCached("https://stat.ripe.net/data/routing-status/data.json?resource=" + encodeURIComponent(prefix)), fetchRipeStatCached("https://stat.ripe.net/data/visibility/data.json?resource=" + encodeURIComponent(prefix)), ]); const origins = routingStatus?.data?.origins || []; const firstSeen = routingStatus?.data?.first_seen?.time || null; // RPKI validation: use local PostgreSQL database (sub-10ms, zero external API calls) let rpkiStatus = "unknown"; let rpkiRoas = []; const originAsn = origins.length > 0 ? origins[0].asn : null; if (originAsn) { try { const localRpki = await validateRPKIWithCache(originAsn, prefix); rpkiStatus = localRpki.status; rpkiRoas = new Array(localRpki.validating_roas); // count only, no detail } catch (e) { console.error("[Prefix Detail] RPKI validation error:", e.message); rpkiStatus = "unknown"; rpkiRoas = []; } } var visData = visibility?.data?.visibilities || []; var risPeersSeeingIt = visData.length > 0 ? visData.filter(v => v.ris_peers_seeing > 0).length : 0; var visibilitySource = "ripe_stat"; // bgproutes.io fallback if RIPE Stat visibility returned no data if (visData.length === 0 && BGPROUTES_API_KEY) { var bgprVis = await fetchBgproutesVisibility(prefix); if (bgprVis && bgprVis.vps_seeing > 0) { risPeersSeeingIt = bgprVis.vps_seeing; visData = []; // keep empty, use risPeersSeeingIt visibilitySource = "bgproutes.io"; } } // Try to get IRR data let irrStatus = "unknown"; try { const whoisData = await fetchRipeStatCached("https://stat.ripe.net/data/whois/data.json?resource=" + encodeURIComponent(prefix)); const records = whoisData?.data?.records || []; if (records.length > 0) irrStatus = "found"; } catch(_e) {} const duration = Date.now() - start; return res.end(JSON.stringify({ meta: { duration_ms: duration, timestamp: new Date().toISOString() }, prefix: prefix, origins: origins.map(o => ({ asn: o.asn, prefix: o.prefix })), rpki: { status: rpkiStatus, validating_roas: rpkiRoas.length }, irr_status: irrStatus, visibility: { ris_peers_seeing: risPeersSeeingIt, total_probes: visData.length || risPeersSeeingIt, source: visibilitySource }, first_seen: firstSeen, }, null, 2)); } catch (err) { res.writeHead(500); return res.end(JSON.stringify({ error: "Prefix detail failed", message: err.message })); } } module.exports = { handlePrefixDetail };