PeerCortex/server/routes/prefix-detail.js
Rene Fichtmueller 49aa46fef3 refactor: extract relationships/compare/quick-ix/peers-find/prefix-detail/ix-detail/topology/whois/enrich routes
Bug fix (agreed pre-existing bug #2 of 3): /api/compare called
fetchRPKIPerPrefix(asn, prefix), never defined anywhere in the codebase --
threw ReferenceError whenever either ASN had >=1 sample prefix, caught by
the outer try/catch, returning 500. Replaced both call sites with
validateRPKIWithCache(asn, prefix), the existing single-prefix RPKI check
used everywhere else in the codebase -- its return shape ({status, ...})
is exactly what the surrounding code already expects.

Verified via node --check, eslint no-undef (only 1 known bug remains:
webhooks POST's `secret`), and smoke-test harness (28/28 match). The
compare fix itself isn't exercised by the smoke test's fixed test ASNs
(both return empty prefix samples locally), but the fix is a direct,
same-shape substitution of an already-proven function.

server.js: 1736 -> 989 lines.
2026-07-17 07:57:04 +02:00

78 lines
3.3 KiB
JavaScript

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 };