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.
83 lines
2.9 KiB
JavaScript
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 };
|