const { fetchRipeStatCached } = require("../services/ripe-stat"); const { resolveASNames } = require("../resolve-as-names"); const { ensureAspaCache, lookupAspaFromRpki } = require("../services/rpki"); const { aspaResultCache, resultCacheGet, resultCacheSet } = require("../caches/response-caches"); // ASPA Check endpoint: /api/aspa?asn=X (existing, kept for compat) async function handleAspaLegacy(req, res, url) { const rawAsn = (url.searchParams.get("asn") || "").replace(/[^0-9]/g, ""); if (!rawAsn) { res.writeHead(400); return res.end(JSON.stringify({ error: "Missing or invalid ASN parameter" })); } const cachedAspa = resultCacheGet(aspaResultCache, rawAsn); if (cachedAspa !== undefined) { res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(cachedAspa)); } const start = Date.now(); let _aspaDone = false; const _aspaTimer = setTimeout(() => { if (!_aspaDone) { _aspaDone = true; res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "ASPA data temporarily unavailable (timeout)", asn: parseInt(rawAsn) })); } }, 12000); try { const [lgData, neighbourData] = await Promise.all([ fetchRipeStatCached("https://stat.ripe.net/data/looking-glass/data.json?resource=AS" + rawAsn, { timeout: 3000 }).catch(() => null), fetchRipeStatCached("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + rawAsn, { timeout: 4000 }), ]); const rrcs = lgData?.data?.rrcs || []; const asPaths = []; rrcs.forEach((rrc) => { const peers = rrc.peers || []; peers.forEach((peer) => { const path = peer.as_path || ""; const pathArr = path.split(" ").map(Number).filter(Boolean); if (pathArr.length > 1) { asPaths.push({ rrc: rrc.rrc, path: pathArr, prefix: peer.prefix || "" }); } }); }); // Provider detection: ONLY use RIPE Stat "left" neighbours (verified upstreams) const neighbours = neighbourData?.data?.neighbours || []; const leftNeighbours = neighbours.filter((n) => n.type === "left"); const upstreamSet = new Set(); leftNeighbours.forEach((n) => upstreamSet.add(n.asn)); // Classify left neighbours: high-power = likely upstream, low-power = likely peer const maxPower = leftNeighbours.reduce((m, n) => Math.max(m, n.power || 0), 1); const detectedProviders = [...upstreamSet].map((asn) => { const nb = leftNeighbours.find((n) => n.asn === asn); const power = nb ? (nb.power || 0) : 0; const powerPct = Math.round((power / maxPower) * 100); const classification = powerPct >= 10 ? "likely_upstream" : "likely_peer"; return { asn, name: nb && nb.as_name ? nb.as_name : "", power, power_pct: powerPct, classification }; }); await resolveASNames(detectedProviders); // Check Cloudflare RPKI feed for ASPA object await ensureAspaCache(); const aspaLookup = lookupAspaFromRpki(rawAsn); const aspaObjectExists = aspaLookup.exists; const aspaDeclaredProviders = aspaLookup.providers; const providerList = detectedProviders.map((p) => "AS" + p.asn).join(", "); let recommendedAspa = "aut-num: AS" + rawAsn + "\n" + "# Recommended ASPA object:\n" + "# customer: AS" + rawAsn + "\n" + "# provider-set: " + providerList + "\n" + "# AFI: ipv4, ipv6\n" + "#\n" + "# Detected providers from BGP path analysis:\n" + detectedProviders.map((p) => "# AS" + p.asn + (p.name ? " (" + p.name + ")" : "")).join("\n"); // If ASPA object exists, show RPKI-declared providers if (aspaObjectExists && aspaDeclaredProviders.length > 0) { recommendedAspa += "\n#\n# RPKI-declared providers (from Cloudflare RPKI feed):\n" + aspaDeclaredProviders.map((a) => "# AS" + a).join("\n"); } const samplePaths = asPaths.slice(0, 10).map((p) => { const pathStr = p.path.map((a) => "AS" + a).join(" -> "); const idx = p.path.indexOf(parseInt(rawAsn)); const provider = idx > 0 ? p.path[idx - 1] : null; return { rrc: p.rrc, prefix: p.prefix, path: pathStr, detected_provider: provider ? "AS" + provider : null, provider_in_set: provider ? upstreamSet.has(provider) : false, }; }); if (_aspaDone) return; // hard timeout already responded _aspaDone = true; clearTimeout(_aspaTimer); const duration = Date.now() - start; const aspaResult = { meta: { query: "AS" + rawAsn, duration_ms: duration, timestamp: new Date().toISOString() }, asn: parseInt(rawAsn), detected_providers: detectedProviders, provider_count: detectedProviders.length, aspa_object_exists: aspaObjectExists, aspa_feed_healthy: aspaLookup.feedLoaded, aspa_declared_providers: aspaDeclaredProviders.map((a) => ({ asn: a })), aspa_declared_count: aspaDeclaredProviders.length, recommended_aspa: recommendedAspa, path_analysis: { total_paths_seen: asPaths.length, sample_paths: samplePaths, }, }; resultCacheSet(aspaResultCache, rawAsn, aspaResult); return res.end(JSON.stringify(aspaResult, null, 2)); } catch (err) { if (!_aspaDone) { _aspaDone = true; clearTimeout(_aspaTimer); res.writeHead(500); return res.end(JSON.stringify({ error: "ASPA check failed", message: err.message })); } } } module.exports = { handleAspaLegacy };