From b82c61e03c0cfaeb7af45a30bfb98e5412857119 Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Fri, 17 Jul 2026 07:36:10 +0200 Subject: [PATCH] refactor: extract lia.js and health.js routes, fix /api/health closure bug Bug fix (agreed pre-existing bug #3 of 3): the .catch() branch of localDb.getLocalDbStats() referenced status/roaAge, both only declared inside the sibling .then() callback's closure -- out of scope in .catch(), so any real DB-stats failure threw ReferenceError with no further catch, hanging the response until client timeout instead of degrading gracefully. Fixed: roaAge computed once before the promise chain (same pattern as the existing aspaAge); the .catch() branch gets its own locally-scoped `status = "degraded"` (a DB-stats fetch failure can't confirm "ok", matching the honest-degradation pattern already used throughout this codebase). Also deduplicated the identical delta_last computation that was repeated verbatim in both branches into one aspaAdoptionDelta() helper. Verified via node --check, eslint no-undef (2 known bugs remain: compare, webhooks), and smoke-test harness (28/28 match). server.js: 3556 -> 3346 lines. --- server.js | 177 ++-------------------------------------- server/routes/health.js | 104 +++++++++++++++++++++++ server/routes/lia.js | 98 ++++++++++++++++++++++ 3 files changed, 210 insertions(+), 169 deletions(-) create mode 100644 server/routes/health.js create mode 100644 server/routes/lia.js diff --git a/server.js b/server.js index 7b66ddb..d99af1b 100644 --- a/server.js +++ b/server.js @@ -220,6 +220,8 @@ function proxyToApiServer(req, res, targetUrl) { const staticRoutes = require("./server/routes/static"); const searchRoutes = require("./server/routes/search"); const feedbackRoutes = require("./server/routes/feedback"); +const liaRoutes = require("./server/routes/lia"); +const healthRoute = require("./server/routes/health"); const server = http.createServer(async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); @@ -282,197 +284,34 @@ const server = http.createServer(async (req, res) => { return staticRoutes.handleFavicon(req, res); } - // Lia'''s Atlas Paradise - Easter egg page + // Lia's Atlas Paradise - Easter egg page if (reqPath === "/lia" || reqPath === "/lia/") { return staticRoutes.handleLia(req, res); } - - - // ============================================================ // Lia's Atlas Paradise: Atlas probe coverage endpoint - // ============================================================ if (reqPath === "/api/atlas/coverage") { - res.setHeader("Content-Type", "application/json"); - if (!atlasState.cache) { - res.writeHead(503); - return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." })); - } - return res.end(JSON.stringify(atlasState.cache, null, 2)); + return liaRoutes.handleAtlasCoverage(req, res); } - // ============================================================ // Lia's Paradise: File parsing endpoint (for binary uploads) - // ============================================================ if (reqPath === "/api/lia/parse-file" && req.method === "POST") { - res.setHeader("Content-Type", "application/json"); - let body = ""; - req.on("data", function(chunk) { body += chunk; }); - req.on("end", function() { - try { - var parsed = JSON.parse(body); - var filename = parsed.filename || ""; - var ext = filename.split(".").pop().toLowerCase(); - // For text-based formats, decode base64 and extract text - if (ext === "csv" || ext === "txt") { - var text = Buffer.from(parsed.data, "base64").toString("utf8"); - return res.end(JSON.stringify({ text: text })); - } - // For binary formats (PDF, XLS, DOC), we can't parse server-side without - // heavy dependencies. Return helpful error. - return res.end(JSON.stringify({ - error: "Binary file parsing (" + ext.toUpperCase() + ") requires client-side extraction. Please use CSV or TXT format, or copy-paste the content.", - suggestion: "Export your spreadsheet as CSV first, then upload the CSV file." - })); - } catch(e) { - return res.end(JSON.stringify({ error: "Parse error: " + e.message })); - } - }); - return; + return liaRoutes.handleParseFile(req, res); } - // ============================================================ // Lia's Paradise: Combined PeeringDB + Atlas coverage data - // ============================================================ if (reqPath === "/api/lia/coverage") { - res.setHeader("Content-Type", "application/json"); - if (!atlasState.cache) { - res.writeHead(503); - return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." })); - } - - // Cache this expensive response for 30 min - var liaCacheKey = "lia_coverage"; - var liaCached = cacheGet(liaCacheKey); - if (liaCached) return res.end(liaCached); - - // Fetch PeeringDB network list (all networks with status "ok") - // Use pre-cached org→country map (loaded at startup, 16MB response cached in memory) - fetchPeeringDB("/net?status=ok&depth=0").then(function(pdbData) { - if (!pdbData || !pdbData.data) { - return res.end(JSON.stringify({ error: "Could not fetch PeeringDB networks" })); - } - - var probeAsns = new Set(atlasState.cache.asns_with_probes || []); - - var enriched = pdbData.data.map(function(n) { - var org = pdbOrgState.map.get(n.org_id) || {}; - var cc = org.country || ""; - return { - asn: n.asn, - name: n.name || "", - org_name: org.name || "", - country: cc, - country_name: cc, - info_type: n.info_type || "", - has_probe: probeAsns.has(n.asn), - }; - }).filter(function(n) { return n.asn > 0 && n.country; }); - - var result = JSON.stringify({ - networks: enriched, - total: enriched.length, - with_probes: enriched.filter(function(n) { return n.has_probe; }).length, - without_probes: enriched.filter(function(n) { return !n.has_probe; }).length, - atlas_unique_asns: probeAsns.size, - org_countries_loaded: pdbOrgState.map.size, - fetched_at: new Date().toISOString(), - }); - - cacheSet(liaCacheKey, result, 30 * 60 * 1000); - res.end(result); - }).catch(function(e) { - res.end(JSON.stringify({ error: "PeeringDB fetch failed: " + e.message })); - }); - return; + return liaRoutes.handleLiaCoverage(req, res); } res.setHeader("Content-Type", "application/json"); // Health endpoint — extended with cache status, ASPA metrics, and local DB stats if (reqPath === "/api/health") { - const mem = process.memoryUsage(); - const rpkiAspaLastFetch = getRpkiAspaLastFetch(); - const aspaAge = rpkiAspaLastFetch ? Math.floor((Date.now() - rpkiAspaLastFetch) / 60000) : -1; - const pdbTotal = pdbSourceCache.hits + pdbSourceCache.misses; - - // Query local DB stats (async, but return partial if needed) - localDb.getLocalDbStats().then(function(dbStats) { - // Determine health status based on local DB data availability - const hasLocalBgp = dbStats && dbStats.bgp_routes > 100000; // should have >2M rows normally - const hasLocalRpki = dbStats && dbStats.rpki_roas > 100000; // should have >500k rows normally - const status = (hasLocalBgp && hasLocalRpki && aspaAge < 300) ? "ok" : "degraded"; - - const healthResponse = { - status, - service: "PeerCortex", - version: "0.6.9", - timestamp: new Date().toISOString(), - uptime_seconds: Math.floor(process.uptime()), - memory_mb: Math.round(mem.heapUsed / 1024 / 1024), - bgproutes_configured: !!BGPROUTES_API_KEY, - caches: { - aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge }, - pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 }, - pdb_netixlan: { entries: pdbSourceCache.netixlan.size }, - pdb_netfac: { entries: pdbSourceCache.netfac.size }, - ripe_stat: { entries: ripeStatCache.size }, - response_cache: { entries: responseCache.size }, - }, - local_db: dbStats ? { - bgp_routes: dbStats.bgp_routes, - rpki_roas: dbStats.rpki_roas, - threat_intel: dbStats.threat_intel, - rdap_cache_entries: dbStats.rdap_cache_entries, - source: "PostgreSQL (local)", - healthy: hasLocalBgp && hasLocalRpki, - } : null, - aspa_adoption: { - total_objects: rpkiAspaMap.size, - roa_count: roaStore.count, - history_samples: aspaAdoptionDailyHistory.length, - delta_last: aspaAdoptionDailyHistory.length >= 2 - ? aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1].aspa_objects - aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2].aspa_objects - : 0, - }, - }; - return res.end(JSON.stringify(healthResponse, null, 2)); - }).catch(function(e) { - console.error('[/api/health] Local DB stats error:', e.message); - // Return health without local DB stats on error - return res.end( - JSON.stringify({ - status, - service: "PeerCortex", - version: "0.6.9", - timestamp: new Date().toISOString(), - uptime_seconds: Math.floor(process.uptime()), - memory_mb: Math.round(mem.heapUsed / 1024 / 1024), - bgproutes_configured: !!BGPROUTES_API_KEY, - caches: { - roa_store: { entries: roaStore.count, age_minutes: roaAge, ready: roaStore.ready }, - aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge }, - pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 }, - pdb_netixlan: { entries: pdbSourceCache.netixlan.size }, - pdb_netfac: { entries: pdbSourceCache.netfac.size }, - ripe_stat: { entries: ripeStatCache.size }, - response_cache: { entries: responseCache.size }, - }, - local_db: { error: "Could not fetch local DB stats", message: e.message }, - aspa_adoption: { - total_objects: rpkiAspaMap.size, - roa_count: roaStore.count, - history_samples: aspaAdoptionDailyHistory.length, - delta_last: aspaAdoptionDailyHistory.length >= 2 - ? aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1].aspa_objects - aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2].aspa_objects - : 0, - }, - }, null, 2) - ); - }); - return; + return healthRoute.handleHealth(req, res); } + // ============================================================ // ASPA Deep Verification endpoint: /api/aspa/verify?asn=X // ============================================================ diff --git a/server/routes/health.js b/server/routes/health.js new file mode 100644 index 0000000..f4d0319 --- /dev/null +++ b/server/routes/health.js @@ -0,0 +1,104 @@ +const localDb = require("../../local-db-client"); +const { getRpkiAspaLastFetch, rpkiAspaMap } = require("../services/rpki"); +const { pdbSourceCache } = require("../caches/pdb-source-cache"); +const { roaStore } = require("../caches/roa-store"); +const { ripeStatCache } = require("../services/ripe-stat"); +const { responseCache } = require("../caches/response-caches"); +const { aspaAdoptionDailyHistory } = require("../aspa-adoption/tracker"); + +const BGPROUTES_API_KEY = process.env.BGPROUTES_API_KEY || ""; + +function aspaAdoptionDelta() { + return aspaAdoptionDailyHistory.length >= 2 + ? aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1].aspa_objects - aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2].aspa_objects + : 0; +} + +// Health endpoint — extended with cache status, ASPA metrics, and local DB stats +function handleHealth(req, res) { + const mem = process.memoryUsage(); + const rpkiAspaLastFetch = getRpkiAspaLastFetch(); + const aspaAge = rpkiAspaLastFetch ? Math.floor((Date.now() - rpkiAspaLastFetch) / 60000) : -1; + // Bug fix 2026-07-17: previously only declared inside the .then() callback + // below, so the .catch() branch's reference to it threw ReferenceError + // (never caught -- the response just hung until client timeout). + const roaAge = roaStore.lastBuild ? Math.floor((Date.now() - roaStore.lastBuild) / 60000) : -1; + const pdbTotal = pdbSourceCache.hits + pdbSourceCache.misses; + + // Query local DB stats (async, but return partial if needed) + localDb.getLocalDbStats().then(function(dbStats) { + // Determine health status based on local DB data availability + const hasLocalBgp = dbStats && dbStats.bgp_routes > 100000; // should have >2M rows normally + const hasLocalRpki = dbStats && dbStats.rpki_roas > 100000; // should have >500k rows normally + const status = (hasLocalBgp && hasLocalRpki && aspaAge < 300) ? "ok" : "degraded"; + + const healthResponse = { + status, + service: "PeerCortex", + version: "0.6.9", + timestamp: new Date().toISOString(), + uptime_seconds: Math.floor(process.uptime()), + memory_mb: Math.round(mem.heapUsed / 1024 / 1024), + bgproutes_configured: !!BGPROUTES_API_KEY, + caches: { + aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge }, + pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 }, + pdb_netixlan: { entries: pdbSourceCache.netixlan.size }, + pdb_netfac: { entries: pdbSourceCache.netfac.size }, + ripe_stat: { entries: ripeStatCache.size }, + response_cache: { entries: responseCache.size }, + }, + local_db: dbStats ? { + bgp_routes: dbStats.bgp_routes, + rpki_roas: dbStats.rpki_roas, + threat_intel: dbStats.threat_intel, + rdap_cache_entries: dbStats.rdap_cache_entries, + source: "PostgreSQL (local)", + healthy: hasLocalBgp && hasLocalRpki, + } : null, + aspa_adoption: { + total_objects: rpkiAspaMap.size, + roa_count: roaStore.count, + history_samples: aspaAdoptionDailyHistory.length, + delta_last: aspaAdoptionDelta(), + }, + }; + return res.end(JSON.stringify(healthResponse, null, 2)); + }).catch(function(e) { + console.error('[/api/health] Local DB stats error:', e.message); + // Return health without local DB stats on error. Local DB stats failed + // to fetch, so "ok" can't be confirmed -- report degraded, matching the + // honest-degradation pattern used throughout this codebase. + const status = "degraded"; + return res.end( + JSON.stringify({ + status, + service: "PeerCortex", + version: "0.6.9", + timestamp: new Date().toISOString(), + uptime_seconds: Math.floor(process.uptime()), + memory_mb: Math.round(mem.heapUsed / 1024 / 1024), + bgproutes_configured: !!BGPROUTES_API_KEY, + caches: { + roa_store: { entries: roaStore.count, age_minutes: roaAge, ready: roaStore.ready }, + aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge }, + pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 }, + pdb_netixlan: { entries: pdbSourceCache.netixlan.size }, + pdb_netfac: { entries: pdbSourceCache.netfac.size }, + ripe_stat: { entries: ripeStatCache.size }, + response_cache: { entries: responseCache.size }, + }, + local_db: { error: "Could not fetch local DB stats", message: e.message }, + aspa_adoption: { + total_objects: rpkiAspaMap.size, + roa_count: roaStore.count, + history_samples: aspaAdoptionDailyHistory.length, + delta_last: aspaAdoptionDelta(), + }, + }, null, 2) + ); + }); + return; +} + +module.exports = { handleHealth }; diff --git a/server/routes/lia.js b/server/routes/lia.js new file mode 100644 index 0000000..45de159 --- /dev/null +++ b/server/routes/lia.js @@ -0,0 +1,98 @@ +const { atlasState } = require("../atlas-probes"); +const { pdbOrgState } = require("../pdb-org-countries"); +const { fetchPeeringDB } = require("../services/peeringdb"); +const { cacheGet, cacheSet } = require("../caches/response-caches"); + +// Lia's Atlas Paradise: Atlas probe coverage endpoint +function handleAtlasCoverage(req, res) { + res.setHeader("Content-Type", "application/json"); + if (!atlasState.cache) { + res.writeHead(503); + return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." })); + } + return res.end(JSON.stringify(atlasState.cache, null, 2)); +} + +// Lia's Paradise: File parsing endpoint (for binary uploads) +function handleParseFile(req, res) { + res.setHeader("Content-Type", "application/json"); + let body = ""; + req.on("data", function(chunk) { body += chunk; }); + req.on("end", function() { + try { + var parsed = JSON.parse(body); + var filename = parsed.filename || ""; + var ext = filename.split(".").pop().toLowerCase(); + // For text-based formats, decode base64 and extract text + if (ext === "csv" || ext === "txt") { + var text = Buffer.from(parsed.data, "base64").toString("utf8"); + return res.end(JSON.stringify({ text: text })); + } + // For binary formats (PDF, XLS, DOC), we can't parse server-side without + // heavy dependencies. Return helpful error. + return res.end(JSON.stringify({ + error: "Binary file parsing (" + ext.toUpperCase() + ") requires client-side extraction. Please use CSV or TXT format, or copy-paste the content.", + suggestion: "Export your spreadsheet as CSV first, then upload the CSV file." + })); + } catch(e) { + return res.end(JSON.stringify({ error: "Parse error: " + e.message })); + } + }); + return; +} + +// Lia's Paradise: Combined PeeringDB + Atlas coverage data +function handleLiaCoverage(req, res) { + res.setHeader("Content-Type", "application/json"); + if (!atlasState.cache) { + res.writeHead(503); + return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." })); + } + + // Cache this expensive response for 30 min + var liaCacheKey = "lia_coverage"; + var liaCached = cacheGet(liaCacheKey); + if (liaCached) return res.end(liaCached); + + // Fetch PeeringDB network list (all networks with status "ok") + // Use pre-cached org→country map (loaded at startup, 16MB response cached in memory) + fetchPeeringDB("/net?status=ok&depth=0").then(function(pdbData) { + if (!pdbData || !pdbData.data) { + return res.end(JSON.stringify({ error: "Could not fetch PeeringDB networks" })); + } + + var probeAsns = new Set(atlasState.cache.asns_with_probes || []); + + var enriched = pdbData.data.map(function(n) { + var org = pdbOrgState.map.get(n.org_id) || {}; + var cc = org.country || ""; + return { + asn: n.asn, + name: n.name || "", + org_name: org.name || "", + country: cc, + country_name: cc, + info_type: n.info_type || "", + has_probe: probeAsns.has(n.asn), + }; + }).filter(function(n) { return n.asn > 0 && n.country; }); + + var result = JSON.stringify({ + networks: enriched, + total: enriched.length, + with_probes: enriched.filter(function(n) { return n.has_probe; }).length, + without_probes: enriched.filter(function(n) { return !n.has_probe; }).length, + atlas_unique_asns: probeAsns.size, + org_countries_loaded: pdbOrgState.map.size, + fetched_at: new Date().toISOString(), + }); + + cacheSet(liaCacheKey, result, 30 * 60 * 1000); + res.end(result); + }).catch(function(e) { + res.end(JSON.stringify({ error: "PeeringDB fetch failed: " + e.message })); + }); + return; +} + +module.exports = { handleAtlasCoverage, handleParseFile, handleLiaCoverage };