Final Phase C batch: moves the remaining inline route bodies (12 proxy stubs, /changelog, the webhooks/hijacks group, PDF export, ASPA adoption tracker + IPv6 stats endpoints) into server/routes/*.js and server/services/api-proxy.js, verified byte-for-byte via the smoke-test diff. Also drops the now-duplicate proxyToApiServer()/API_SERVER_PORT that were left behind in server.js after api-proxy.js absorbed them. server.js: 6838 -> 578 lines, under the 800-line project limit.
579 lines
20 KiB
JavaScript
579 lines
20 KiB
JavaScript
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);
|
|
});
|