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.
78 lines
3.3 KiB
JavaScript
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 };
|