fetchBgpHeNet moved as-is, not removed, despite being confirmed dead code in practice (its only call site in /api/lookup is hardcoded to Promise.resolve(null)) -- that's a "disabled pending re-enable" state, a bigger behavioral decision than this extraction pass should make alone. Also widened the smoke-test harness's volatile-path exclusions: the live Cloudflare RPKI feed's ASPA object count ticks between runs (independent of any code change here), and the field appears under three different names (aspa_map.entries, aspa_adoption.total_objects, plus the derived delta_from_previous) that weren't all covered by the existing aspa_objects exclusion. Re-captured baseline accordingly. Verified via smoke-test harness (28/28 match). server.js: 4827 -> 4575 lines.
50 lines
2.5 KiB
JavaScript
50 lines
2.5 KiB
JavaScript
const { fetchRipeStatCached } = require("./services/ripe-stat");
|
|
|
|
// Feature 25: Topology / AS-Relationships
|
|
async function fetchTopology(targetAsn, depth) {
|
|
const maxDepth = Math.min(depth || 2, 3);
|
|
const nodes = new Map();
|
|
const edges = [];
|
|
async function fetchNeighboursForAsn(asn, currentDepth) {
|
|
if (nodes.has(asn) && nodes.get(asn).depth <= currentDepth) return;
|
|
const [data, overview] = await Promise.all([
|
|
fetchRipeStatCached("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + asn),
|
|
fetchRipeStatCached("https://stat.ripe.net/data/as-overview/data.json?resource=AS" + asn),
|
|
]);
|
|
const name = overview?.data?.holder || "";
|
|
const neighbours = data?.data?.neighbours || [];
|
|
const upstreams = neighbours.filter((n) => n.type === "left");
|
|
const downstreams = neighbours.filter((n) => n.type === "right");
|
|
const peers = neighbours.filter((n) => n.type === "uncertain" || n.type === "peer");
|
|
const nodeType = asn === targetAsn ? "target" : currentDepth === 1 ? "direct" : "indirect";
|
|
nodes.set(asn, { asn, name, type: nodeType, depth: currentDepth });
|
|
upstreams.forEach((n) => {
|
|
if (!nodes.has(n.asn)) nodes.set(n.asn, { asn: n.asn, name: n.as_name || "", type: "upstream", depth: currentDepth + 1 });
|
|
edges.push({ from: n.asn, to: asn, relationship: "provider-to-customer" });
|
|
});
|
|
downstreams.forEach((n) => {
|
|
if (!nodes.has(n.asn)) nodes.set(n.asn, { asn: n.asn, name: n.as_name || "", type: "downstream", depth: currentDepth + 1 });
|
|
edges.push({ from: asn, to: n.asn, relationship: "provider-to-customer" });
|
|
});
|
|
peers.slice(0, 10).forEach((n) => {
|
|
if (!nodes.has(n.asn)) nodes.set(n.asn, { asn: n.asn, name: n.as_name || "", type: "peer", depth: currentDepth + 1 });
|
|
edges.push({ from: asn, to: n.asn, relationship: "peer" });
|
|
});
|
|
if (currentDepth < maxDepth && upstreams.length > 0) {
|
|
const top5 = upstreams.sort((a, b) => (b.power || 0) - (a.power || 0)).slice(0, 5);
|
|
await Promise.all(top5.map((u) => fetchNeighboursForAsn(u.asn, currentDepth + 1)));
|
|
}
|
|
}
|
|
await fetchNeighboursForAsn(targetAsn, 0);
|
|
const edgeSet = new Set();
|
|
const uniqueEdges = edges.filter((e) => {
|
|
const key = e.from + "-" + e.to + "-" + e.relationship;
|
|
if (edgeSet.has(key)) return false;
|
|
edgeSet.add(key);
|
|
return true;
|
|
});
|
|
return { nodes: [...nodes.values()], edges: uniqueEdges, target_asn: targetAsn, depth: maxDepth };
|
|
}
|
|
|
|
module.exports = { fetchTopology };
|