const fs = require("fs"); const http = require("http"); const https = require("https"); const crypto = require("crypto"); // ── LOCAL DATABASE CLIENT (BGP, RPKI, Threat Intel) ────────────── const localDb = require('./local-db-client'); console.log('[PeerCortex] Local DB client initialized'); // Load .env file require('./src/backend/config'); const BGPROUTES_API_KEY = process.env.BGPROUTES_API_KEY || ""; const BGPROUTES_API_URL = process.env.BGPROUTES_API_URL || "https://api.bgproutes.io/v1"; // bio-rd gRPC client (optional — graceful fallback if unavailable) let risClient = null; try { const { createRisClient } = require('./bio-rd-client'); const BIO_RD_HOST = process.env.BIO_RD_HOST || 'localhost'; const BIO_RD_PORT = parseInt(process.env.BIO_RD_PORT || '4321'); risClient = createRisClient(BIO_RD_HOST, BIO_RD_PORT); console.log(`[bio-rd] RIS client configured → ${BIO_RD_HOST}:${BIO_RD_PORT}`); } catch (e) { console.log('[bio-rd] RIS client not available (bio-rd-client.js missing or gRPC not installed)'); } const { PEERINGDB_API_KEY, PEERINGDB_API_URL, getPdbLocal, queryPeeringDBLocal, pdbSemaphore, fetchPeeringDB, fetchPeeringDBWithRetry, } = require("./server/services/peeringdb"); // ── SMTP / Email ────────────────────────────────────────────── const { sendFeedbackMail } = require('./src/backend/services/smtp'); // ── SMTP / Email ────────────────────────────────────────────── const { loadVisitors, trackVisitor } = require("./server/visitors"); // ═══════════════════════════════════════════════════════════════ // PEERCORTEX v0.6.1 — New Features // ═══════════════════════════════════════════════════════════════ // ── BGP Community Database ───────────────────────────────────── // Migrated to src/features/bgp-communities/bgp-community-db.ts // ── Hijack Monitoring ────────────────────────────────────────── const { DATA_DIR, loadHijackSubs, loadHijackAlerts, loadWebhookSubs, saveWebhookSubs, saveHijackAlerts, saveHijackSubs, } = require("./server/hijack-monitoring/store"); const { webhookSignature, deliverWebhook, notifyWebhooks } = require("./server/hijack-monitoring/webhooks"); const { classifyHijackSeverity, checkHijacksForAsn, runHijackCheck } = require("./server/hijack-monitoring/monitor"); const { aspaAdoptionDailyHistory, recordAspaAdoptionSnapshot, getAdoptionTrend, buildAspaAdoptionDashboard, } = require("./server/aspa-adoption/tracker"); const { fetchRirDelegation, fetchIpv6AdoptionStats } = require("./server/ipv6-adoption"); const { UA } = require("./server/data/constants"); const { CITY_COORDS } = require("./server/data/city-coords"); // ============================================================ const { scoreRingSvg, pdfBaseCSS, buildPdfHtml, buildComparisonPdfHtml, formatLabel, escHtml, } = require("./server/pdf-export/templates"); const { getPuppeteerBrowser, pdfCacheGet, pdfCacheSet, renderHtmlToPdf, } = require("./server/pdf-export/renderer"); const { responseCache, cacheGet, cacheSet, RESULT_CACHE_TTL, CACHE_TTL_LOOKUP, CACHE_TTL_LOOKUP_DEGRADED, CACHE_TTL_DEFAULT, rdapCacheGet, rdapCacheSet, whoisCacheGet, whoisCacheSet, WHOIS_NOT_FOUND_CACHE_TTL, quickIxCacheGet, quickIxCacheSet, bgproutesResultCache, aspaResultCache, validateResultCache, resultCacheGet, resultCacheSet, } = require("./server/caches/response-caches"); // bgproutesVpCache/bgproutesVpCacheTs/BGPROUTES_VP_TTL removed 2026-07-16: // confirmed dead code, never read anywhere in the file. const { ensureManrsCache, checkManrsMembership } = require("./server/services/manrs"); // ============================================================ // subCableCache/globalFacCache removed 2026-07-16: confirmed dead code, // never read anywhere in the file. const { roaStore } = require("./server/caches/roa-store"); const { rpkiAspaMap, getRpkiAspaLastFetch, fetchRpkiAspaFeed, ensureAspaCache, lookupAspaFromRpki, validateRPKIWithCache, fetchRipeRpkiValidator, crossCheckRpki, } = require("./server/services/rpki"); const { pdbSourceCache } = require("./server/caches/pdb-source-cache"); const { ripeStatCache, fetchRipeStatCached, fetchRipeStatCachedFromApi, fetchRipeStatCachedWithRetry, saveRipeStatCacheToDisk, loadRipeStatCacheFromDisk, Semaphore, } = require("./server/services/ripe-stat"); const { fetchJSON, fetchJSONWithRetry, fetchHTML, postJSON, fetchBgproutesVisibility, } = require("./server/services/http-helpers"); // checkRateLimit/rateLimitMap removed 2026-07-16: dead code, never called by // the request handler (confirmed via the server.js structural exploration). const { resolveASNames } = require("./server/resolve-as-names"); const { hasAsSet, hopCheck, collapsePrepends, verifyUpstream, verifyDownstream, detectValleys, buildAspaStore, calculateAspaReadinessScore, } = require("./server/aspa-verification/engine"); const { fetchBgpHeNet } = require("./server/bgp-he-net"); const { fetchTopology } = require("./server/topology"); const { computeResilienceScore } = require("./server/resilience-score"); const { computeRouteLeakDetection } = require("./server/route-leak"); const { fetchWhois } = require("./server/whois"); // ============================================================ // HTTP Server // ============================================================ 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 aspaVerifyRoute = require("./server/routes/aspa-verify"); const aspaLegacyRoute = require("./server/routes/aspa-legacy"); const bgpRoute = require("./server/routes/bgp"); const validateRoute = require("./server/routes/validate"); const lookupRoute = require("./server/routes/lookup"); const relationshipsRoute = require("./server/routes/relationships"); const compareRoute = require("./server/routes/compare"); const quickIxRoute = require("./server/routes/quick-ix"); const peersFindRoute = require("./server/routes/peers-find"); const prefixDetailRoute = require("./server/routes/prefix-detail"); const ixDetailRoute = require("./server/routes/ix-detail"); const topologyRoute = require("./server/routes/topology"); const whoisRoute = require("./server/routes/whois"); const enrichRoute = require("./server/routes/enrich"); const proxyStubsRoute = require("./server/routes/proxy-stubs"); const changelogRoute = require("./server/routes/changelog"); const hijackAlertsRoute = require("./server/routes/hijack-alerts"); const pdfExportRoute = require("./server/routes/pdf-export"); const aspaAdoptionRoute = require("./server/routes/aspa-adoption"); const server = http.createServer(async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { res.writeHead(204); return res.end(); } let url, reqPath; try { url = new URL(req.url, "http://localhost"); reqPath = url.pathname; } catch (_urlErr) { res.writeHead(400); return res.end('Bad Request'); } // Serve static files — host-based routing const host = (req.headers.host || '').split(':')[0]; if (reqPath === "/" || reqPath === "/index.html") { return staticRoutes.handleRoot(req, res, host, reqPath); } // Direct access to editorial version if (reqPath === "/index-editorial.html") { return staticRoutes.handleIndexEditorial(req, res); } // Name Search (RIPE Stat + PeeringDB combined) if (reqPath === "/api/search") { return searchRoutes.handleSearch(req, res); } // GET /api/visitors — unique visitor count if (reqPath === "/api/visitors" && req.method === "GET") { return searchRoutes.handleVisitors(req, res); } // OPTIONS preflight (CORS) if (reqPath === "/api/feedback" && req.method === "OPTIONS") { return feedbackRoutes.handleOptions(req, res); } // POST /api/feedback — submit feedback entry if (reqPath === "/api/feedback" && req.method === "POST") { return feedbackRoutes.handlePost(req, res); } // GET /api/feedback?token=... — admin: fetch all entries as JSON if (reqPath === "/api/feedback" && req.method === "GET") { return feedbackRoutes.handleGet(req, res, url); } // Serve favicon if (reqPath === "/favicon.ico") { return staticRoutes.handleFavicon(req, res); } // 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") { return liaRoutes.handleAtlasCoverage(req, res); } // Lia's Paradise: File parsing endpoint (for binary uploads) if (reqPath === "/api/lia/parse-file" && req.method === "POST") { return liaRoutes.handleParseFile(req, res); } // Lia's Paradise: Combined PeeringDB + Atlas coverage data if (reqPath === "/api/lia/coverage") { 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") { return healthRoute.handleHealth(req, res); } // ============================================================ // ASPA Deep Verification endpoint: /api/aspa/verify?asn=X if (reqPath === "/api/aspa/verify") { return aspaVerifyRoute.handleAspaVerify(req, res, url); } // ASPA Check endpoint: /api/aspa?asn=X (existing, kept for compat) if (reqPath === "/api/aspa") { return aspaLegacyRoute.handleAspaLegacy(req, res, url); } // BGP endpoint (LOCAL DB): /api/bgp?asn=X (or prefix=X) if (reqPath === "/api/bgp") { return bgpRoute.handleBgp(req, res, url); } // Unified Validation endpoint: /api/validate?asn=X if (reqPath === "/api/validate") { return validateRoute.handleValidate(req, res, url); } // Main lookup endpoint: /api/lookup?asn=X if (reqPath === "/api/lookup") { return lookupRoute.handleLookup(req, res, url); } if (reqPath === "/api/relationships") { return relationshipsRoute.handleRelationships(req, res, url); } // Compare endpoint: /api/compare?asn1=X&asn2=Y if (reqPath === "/api/compare") { return compareRoute.handleCompare(req, res, url); } // Quick-IX endpoint: /api/quick-ix?asn=X if (reqPath === "/api/quick-ix") { return quickIxRoute.handleQuickIx(req, res, url); } // Peer Matching endpoint: /api/peers/find?ix=NAME&policy=open&min_speed=10000 if (reqPath === "/api/peers/find") { return peersFindRoute.handlePeersFind(req, res, url); } // Prefix Detail endpoint: /api/prefix/detail?prefix=X if (reqPath === "/api/prefix/detail") { return prefixDetailRoute.handlePrefixDetail(req, res, url); } // IX Detail endpoint: /api/ix/detail?ix_id=X if (reqPath === "/api/ix/detail") { return ixDetailRoute.handleIxDetail(req, res, url); } // Feature 25: Topology endpoint if (reqPath === "/api/topology") { return topologyRoute.handleTopology(req, res, url); } // Feature 27: WHOIS endpoint if (reqPath === "/api/whois") { return whoisRoute.handleWhoisRoute(req, res, url); } // Feature 28b: Company enrichment via Wikipedia + website meta scraping if (reqPath === "/api/enrich") { return enrichRoute.handleEnrich(req, res, url); } // Feature 28: Submarine Cable overlay + other already-migrated routes, // proxied through to the separate Fastify API server. if (proxyStubsRoute.isProxyStubPath(reqPath)) { return proxyStubsRoute.handleProxyStub(req, res); } // Changelog page if (reqPath === "/changelog") { return changelogRoute.handleChangelog(req, res); } // CORS preflight for all /api/webhooks + /api/hijacks if (hijackAlertsRoute.matchesHijackGroup(reqPath) && req.method === "OPTIONS") { return hijackAlertsRoute.handleOptions(req, res); } // Webhook: Register. POST /api/webhooks?asn=X if (reqPath === "/api/webhooks" && req.method === "POST") { return hijackAlertsRoute.handleWebhookRegister(req, res); } // Webhook: List. GET /api/webhooks?asn=X if (reqPath === "/api/webhooks" && req.method === "GET") { return hijackAlertsRoute.handleWebhookList(req, res); } // Webhook: Delete. DELETE /api/webhooks/:id if (reqPath.startsWith("/api/webhooks/") && req.method === "DELETE" && !reqPath.includes("/test")) { return hijackAlertsRoute.handleWebhookDelete(req, res, reqPath); } // Webhook: Test. POST /api/webhooks/:id/test if (reqPath.match(/^\/api\/webhooks\/[^/]+\/test$/) && req.method === "POST") { return hijackAlertsRoute.handleWebhookTest(req, res, reqPath); } // Hijack Events: List. GET /api/hijacks?asn=X&limit=50&resolved=false if (reqPath === "/api/hijacks" && req.method === "GET") { return hijackAlertsRoute.handleHijacksList(req, res); } // Hijack Events: Resolve. POST /api/hijacks/:id/resolve if (reqPath.match(/^\/api\/hijacks\/[^/]+\/resolve$/) && req.method === "POST") { return hijackAlertsRoute.handleHijackResolve(req, res, reqPath); } // Hijack Subscribe (legacy) if (reqPath === "/api/hijack-subscribe" && req.method === "POST") { return hijackAlertsRoute.handleHijackSubscribe(req, res); } // Hijack Alerts (legacy endpoint) if (reqPath.startsWith("/api/hijack-alerts")) { return hijackAlertsRoute.handleHijackAlertsLegacy(req, res); } // PDF Export if (reqPath === "/api/export/pdf" && req.method === "GET") { return pdfExportRoute.handleExportPdf(req, res, url); } if (reqPath === "/api/export/pdf/compare" && req.method === "GET") { return pdfExportRoute.handleExportPdfCompare(req, res, url); } if (reqPath.startsWith("/api/export/") && req.method === "OPTIONS") { return pdfExportRoute.handleExportOptions(req, res); } // ASPA Adoption Tracker if (reqPath === "/aspa-adoption") { return aspaAdoptionRoute.handleDashboard(req, res); } if (reqPath === "/api/aspa-adoption-stats" && req.method === "GET") { return aspaAdoptionRoute.handleStats(req, res, url); } if (reqPath === "/api/aspa-adoption-stats/export" && req.method === "GET") { return aspaAdoptionRoute.handleStatsExport(req, res, url); } if (reqPath === "/api/ipv6-adoption-stats" && req.method === "GET") { return aspaAdoptionRoute.handleIpv6Stats(req, res, url); } // 404 res.writeHead(404); res.end( JSON.stringify({ error: "Not found. Endpoints: /api/health, /api/validate?asn=X, /api/lookup?asn=X, /api/aspa?asn=X, /api/aspa/verify?asn=X, /api/bgproutes?asn=X, /api/compare?asn1=X&asn2=Y, /api/peers/find?ix=NAME, /api/prefix/detail?prefix=X, /api/ix/detail?ix_id=X, /api/export/pdf?asn=X&format=report|executive|technical, /api/export/pdf/compare?asn1=X&asn2=Y, /api/aspa-adoption-stats?period=30d, /api/ipv6-adoption-stats", }) ); }); const { atlasState, fetchAllAtlasProbes } = require("./server/atlas-probes"); const { pdbOrgState, fetchPdbOrgCountries } = require("./server/pdb-org-countries"); const PORT = process.env.PORT || 3101; // ============================================================ // Startup Sequence — load disk caches first, then fetch fresh data // ============================================================ // Phase 0: Load disk caches for fast restart (instant) roaStore.loadFromDisk("/opt/peercortex-app/.roa-cache.json"); pdbSourceCache.loadFromDisk("/opt/peercortex-app/.pdb-source-cache.json"); loadRipeStatCacheFromDisk("/opt/peercortex-app/.ripe-stat-cache.json"); // Phase 1: Fetch fresh RPKI feed (ASPA + ROA) + Atlas probes + PDB org countries + MANRS participants ensureManrsCache(); // fire-and-forget, 24h cache Promise.all([fetchRpkiAspaFeed(), fetchAllAtlasProbes(), fetchPdbOrgCountries()]).then(() => { // Re-record ASPA adoption snapshot now that Atlas data is fully loaded recordAspaAdoptionSnapshot(rpkiAspaMap.size, roaStore.count, rpkiAspaMap); server.listen(PORT, "0.0.0.0", () => { console.log("PeerCortex v0.6.5 running on http://0.0.0.0:" + PORT); console.log("bgproutes.io API key: " + (BGPROUTES_API_KEY ? "configured" : "NOT configured")); console.log("PeeringDB API key: " + (PEERINGDB_API_KEY ? "configured" : "NOT configured")); console.log("RPKI ASPA objects: " + rpkiAspaMap.size); console.log("ROA store: " + roaStore.count + " entries (" + (roaStore.ready ? "ready" : "loading...") + ")"); console.log("PDB source cache: net=" + pdbSourceCache.net.size + " ix=" + pdbSourceCache.netixlan.size + " fac=" + pdbSourceCache.netfac.size); console.log("RIPE Stat cache: " + ripeStatCache.size + " entries"); }); }); // ============================================================ // bio-rd RIB WebSocket — live route streaming on /ws/rib // ============================================================ let WebSocketServer = null; try { WebSocketServer = require('ws').Server; } catch(_e) {} if (WebSocketServer) { const ribWss = new WebSocketServer({ server, path: '/ws/rib' }); ribWss.on('connection', function(ws) { let cancelStream = null; ws.on('message', function(raw) { try { const msg = JSON.parse(raw); if (msg.type === 'rib-subscribe') { if (cancelStream) { cancelStream(); cancelStream = null; } if (!risClient) { ws.send(JSON.stringify({ type: 'error', error: 'bio-rd RIS not configured' })); return; } const router = msg.router || 'default'; cancelStream = risClient.observeRib( router, 'default', function(update) { if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(update)); }, function(err) { if (ws.readyState === ws.OPEN) ws.send(JSON.stringify({ type: 'error', error: err.message })); } ); } } catch(e) { if (ws.readyState === ws.OPEN) ws.send(JSON.stringify({ type: 'error', error: e.message })); } }); ws.on('close', function() { if (cancelStream) { cancelStream(); cancelStream = null; } }); }); console.log('[bio-rd] RIB WebSocket server listening on /ws/rib'); } else { console.log('[bio-rd] WebSocket server skipped (ws package not installed)'); } // ============================================================ // Refresh timers — jittered to avoid thundering herd // ============================================================ // RPKI feed (ASPA + ROA): every 4h ± 5min jitter setInterval(() => { fetchRpkiAspaFeed(); }, 4 * 60 * 60 * 1000 + Math.floor(Math.random() * 10 * 60 * 1000) - 5 * 60 * 1000); // Atlas probe cache: every 12h ± 10min jitter setInterval(function() { fetchAllAtlasProbes(); }, 12 * 60 * 60 * 1000 + Math.floor(Math.random() * 20 * 60 * 1000) - 10 * 60 * 1000); // Disk cache persistence: every 30 minutes setInterval(function() { pdbSourceCache.saveToDisk("/opt/peercortex-app/.pdb-source-cache.json"); saveRipeStatCacheToDisk("/opt/peercortex-app/.ripe-stat-cache.json"); }, 30 * 60 * 1000); // Save caches on graceful shutdown process.on("SIGTERM", function() { console.log("[SHUTDOWN] Saving caches to disk..."); pdbSourceCache.saveToDisk("/opt/peercortex-app/.pdb-source-cache.json"); saveRipeStatCacheToDisk("/opt/peercortex-app/.ripe-stat-cache.json"); roaStore.saveToDisk("/opt/peercortex-app/.roa-cache.json"); process.exit(0); }); process.on("SIGINT", function() { console.log("[SHUTDOWN] Saving caches to disk..."); pdbSourceCache.saveToDisk("/opt/peercortex-app/.pdb-source-cache.json"); saveRipeStatCacheToDisk("/opt/peercortex-app/.ripe-stat-cache.json"); roaStore.saveToDisk("/opt/peercortex-app/.roa-cache.json"); process.exit(0); });