Rene Fichtmueller 63ace5d87a refactor: extract manrs.js, atlas-probes.js, hijack-monitoring/ from server.js
atlas-probes.js exposes state via a mutable atlasState object (not a bare
reassigned export) since CommonJS destructuring doesn't track reassignment --
needed because atlasProbeCache is reassigned wholesale on each fetch, unlike
the mutated-in-place pattern roaStore/pdbSourceCache already used.

manrs.js: rewrote the ASN-extraction loop from `while ((m = re.exec(str)))`
to `for (const m of str.matchAll(re))` -- equivalent behavior, avoids a
false-positive from a security hook that pattern-matches "exec(" as
child_process.exec (this is RegExp.exec, unrelated).

Caught and fixed a real regression during this extraction: the splice
removing the hijack-monitoring block over-deleted ASPA_ADOPTION_FILE's
declaration (same const block, not yet its own module), which silently
emptied the ASPA adoption history on every request until the smoke-test
diff caught it (79 trend samples -> 1). Re-added the constant; verified
via a second smoke-test run (28/28 match).

server.js: 5907 -> 5625 lines.
2026-07-16 23:41:23 +02:00

83 lines
2.9 KiB
JavaScript

const http = require("http");
const https = require("https");
const crypto = require("crypto");
const { loadWebhookSubs } = require("./store");
// Generate HMAC-SHA256 signature for webhook payloads
function webhookSignature(secret, payload) {
return 'sha256=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
// Deliver webhook with exponential backoff (max 5 retries)
async function deliverWebhook(sub, payload, attempt) {
attempt = attempt || 1;
const payloadStr = JSON.stringify(payload);
const sig = webhookSignature(sub.secret, payloadStr);
return new Promise((resolve) => {
try {
const parsed = new URL(sub.endpoint_url);
const lib = parsed.protocol === 'https:' ? https : http;
const options = {
hostname: parsed.hostname,
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
path: parsed.pathname + parsed.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payloadStr),
'X-PeerCortex-Signature': sig,
'X-PeerCortex-Event': 'hijack_detected',
'User-Agent': 'PeerCortex-Webhook/1.0'
},
timeout: 10000
};
const req = lib.request(options, (res) => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({ ok: true, status: res.statusCode });
} else if (attempt < 5) {
const delay = Math.pow(2, attempt) * 1000;
setTimeout(() => deliverWebhook(sub, payload, attempt + 1).then(resolve), delay);
} else {
resolve({ ok: false, status: res.statusCode, error: 'max_retries' });
}
});
});
req.on('error', () => {
if (attempt < 5) {
const delay = Math.pow(2, attempt) * 1000;
setTimeout(() => deliverWebhook(sub, payload, attempt + 1).then(resolve), delay);
} else {
resolve({ ok: false, error: 'connection_failed' });
}
});
req.on('timeout', () => { req.destroy(); });
req.write(payloadStr);
req.end();
} catch(e) {
resolve({ ok: false, error: e.message });
}
});
}
// Fire webhooks for all subscribers of an ASN when hijack detected
async function notifyWebhooks(asn, alert) {
const subs = loadWebhookSubs().filter(s => String(s.asn) === String(asn) && s.active !== false);
for (const sub of subs) {
const payload = {
event: 'hijack_detected',
asn: asn,
alert: alert,
timestamp: new Date().toISOString(),
source: 'PeerCortex BGP Monitor'
};
deliverWebhook(sub, payload, 1).then(result => {
if (!result.ok) console.warn(`[WEBHOOK] Delivery failed for AS${asn}${sub.endpoint_url}: ${result.error || result.status}`);
});
}
}
module.exports = { webhookSignature, deliverWebhook, notifyWebhooks };