feat: BGP hijack alerting + webhook delivery system
- 6 new REST endpoints: POST/GET/DELETE /api/webhooks, POST /api/webhooks/:id/test, GET/POST /api/hijacks - HMAC-SHA256 signed webhook payloads (X-PeerCortex-Signature header) - Exponential backoff retry: 2^attempt * 1000ms, up to 5 attempts - 6-hour deduplication window per (ASN, severity) to prevent alert storms - Severity classification: CRITICAL/HIGH/MEDIUM/LOW based on unexpected/missing ASNs + RPKI invalid - DATA_DIR pattern for portable file storage (dev vs /opt/peercortex-app on Erik) - Backwards-compatible: legacy /api/hijack-subscribe + /api/hijack-alerts routes kept - Fix: 0-prefix cache results now use 90s TTL instead of 15min to prevent stale data
This commit is contained in:
parent
4cf1673e21
commit
e1197aa7d6
289
server.js
289
server.js
@ -360,16 +360,109 @@ function decodeCommunities(communityList) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Hijack Monitoring ──────────────────────────────────────────
|
// ── Hijack Monitoring ──────────────────────────────────────────
|
||||||
const HIJACK_SUBS_FILE = '/opt/peercortex-app/hijack-subs.json';
|
// ============================================================
|
||||||
const HIJACK_ALERTS_FILE = '/opt/peercortex-app/hijack-alerts.json';
|
// FEATURE 1: BGP Hijack Alerting + Webhooks
|
||||||
|
// ============================================================
|
||||||
|
const DATA_DIR = process.env.DATA_DIR || (fs.existsSync('/opt/peercortex-app') ? '/opt/peercortex-app' : __dirname);
|
||||||
|
const HIJACK_SUBS_FILE = DATA_DIR + '/hijack-subs.json';
|
||||||
|
const HIJACK_ALERTS_FILE = DATA_DIR + '/hijack-alerts.json';
|
||||||
|
const WEBHOOK_SUBS_FILE = DATA_DIR + '/webhook-subs.json';
|
||||||
|
|
||||||
function loadHijackSubs() { try { return JSON.parse(fs.readFileSync(HIJACK_SUBS_FILE,'utf8')); } catch(_){ return []; } }
|
function loadHijackSubs() { try { return JSON.parse(fs.readFileSync(HIJACK_SUBS_FILE,'utf8')); } catch(_){ return []; } }
|
||||||
function loadHijackAlerts() { try { return JSON.parse(fs.readFileSync(HIJACK_ALERTS_FILE,'utf8')); } catch(_){ return []; } }
|
function loadHijackAlerts() { try { return JSON.parse(fs.readFileSync(HIJACK_ALERTS_FILE,'utf8')); } catch(_){ return []; } }
|
||||||
|
function loadWebhookSubs() { try { return JSON.parse(fs.readFileSync(WEBHOOK_SUBS_FILE,'utf8')); } catch(_){ return []; } }
|
||||||
|
function saveWebhookSubs(s) { try { fs.writeFileSync(WEBHOOK_SUBS_FILE, JSON.stringify(s, null, 2)); } catch(_){} }
|
||||||
|
function saveHijackAlerts(a) { try { fs.writeFileSync(HIJACK_ALERTS_FILE, JSON.stringify(a.slice(-1000), null, 2)); } catch(_){} }
|
||||||
|
function saveHijackSubs(s) { try { fs.writeFileSync(HIJACK_SUBS_FILE, JSON.stringify(s, null, 2)); } catch(_){} }
|
||||||
|
|
||||||
|
// Generate HMAC-SHA256 signature for webhook payloads
|
||||||
|
function webhookSignature(secret, payload) {
|
||||||
|
return 'sha256=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classify hijack severity
|
||||||
|
function classifyHijackSeverity(unexpected, missing, rpkiInvalid) {
|
||||||
|
if (rpkiInvalid > 0 || unexpected.length >= 5) return 'CRITICAL';
|
||||||
|
if (unexpected.length >= 2 || missing.length >= 3) return 'HIGH';
|
||||||
|
if (unexpected.length >= 1 || missing.length >= 1) return 'MEDIUM';
|
||||||
|
return 'LOW';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkHijacksForAsn(asn) {
|
async function checkHijacksForAsn(asn) {
|
||||||
try {
|
try {
|
||||||
const url = `https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS${asn}&${UA}`;
|
const url = `https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS${asn}`;
|
||||||
const data = await fetchJSON(url, { timeout: 6000 });
|
const data = await fetchJSON(url, { timeout: 8000 });
|
||||||
const prefixes = (data && data.data && data.data.prefixes || []).map(p => p.prefix);
|
const prefixes = (data && data.data && data.data.prefixes || []).map(p => p.prefix);
|
||||||
return prefixes;
|
return prefixes;
|
||||||
} catch (_) { return []; }
|
} catch (_) { return []; }
|
||||||
@ -379,26 +472,36 @@ async function runHijackCheck() {
|
|||||||
const subs = loadHijackSubs();
|
const subs = loadHijackSubs();
|
||||||
if (!subs.length) return;
|
if (!subs.length) return;
|
||||||
const alerts = loadHijackAlerts();
|
const alerts = loadHijackAlerts();
|
||||||
|
let changed = false;
|
||||||
for (const sub of subs) {
|
for (const sub of subs) {
|
||||||
const current = await checkHijacksForAsn(sub.asn);
|
const current = await checkHijacksForAsn(sub.asn);
|
||||||
const baseline = new Set(sub.prefixes || []);
|
const baseline = new Set(sub.prefixes || []);
|
||||||
const unexpected = current.filter(p => baseline.size > 0 && !baseline.has(p));
|
const unexpected = current.filter(p => baseline.size > 0 && !baseline.has(p));
|
||||||
const missing = [...baseline].filter(p => !current.includes(p));
|
const missing = [...baseline].filter(p => !current.includes(p));
|
||||||
if (unexpected.length || missing.length) {
|
if (unexpected.length || missing.length) {
|
||||||
|
// Dedup: only alert if no alert in last 6 hours for this ASN
|
||||||
|
const sixHoursAgo = Date.now() - 6 * 60 * 60 * 1000;
|
||||||
|
const recentAlert = alerts.find(a => a.asn === sub.asn && new Date(a.ts).getTime() > sixHoursAgo);
|
||||||
|
if (!recentAlert) {
|
||||||
|
const severity = classifyHijackSeverity(unexpected, missing, 0);
|
||||||
const alert = {
|
const alert = {
|
||||||
|
id: crypto.randomBytes(8).toString('hex'),
|
||||||
asn: sub.asn, ts: new Date().toISOString(),
|
asn: sub.asn, ts: new Date().toISOString(),
|
||||||
unexpected, missing,
|
severity, unexpected, missing, resolved: false,
|
||||||
msg: `Possible hijack detected for AS${sub.asn}: ${unexpected.length} unexpected, ${missing.length} missing prefixes`
|
msg: `BGP anomaly for AS${sub.asn}: ${unexpected.length} unexpected prefix(es), ${missing.length} missing`
|
||||||
};
|
};
|
||||||
alerts.push(alert);
|
alerts.push(alert);
|
||||||
try { fs.writeFileSync(HIJACK_ALERTS_FILE, JSON.stringify(alerts.slice(-500), null, 2)); } catch(_) {}
|
changed = true;
|
||||||
|
notifyWebhooks(sub.asn, alert);
|
||||||
}
|
}
|
||||||
// Update baseline with current prefixes if no baseline set
|
}
|
||||||
|
// Set baseline on first monitoring
|
||||||
if (!sub.prefixes || !sub.prefixes.length) {
|
if (!sub.prefixes || !sub.prefixes.length) {
|
||||||
sub.prefixes = current;
|
sub.prefixes = current;
|
||||||
try { fs.writeFileSync(HIJACK_SUBS_FILE, JSON.stringify(subs, null, 2)); } catch(_) {}
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (changed) { saveHijackAlerts(alerts); saveHijackSubs(subs); }
|
||||||
}
|
}
|
||||||
// Run hijack check every 30 minutes
|
// Run hijack check every 30 minutes
|
||||||
setInterval(runHijackCheck, 30 * 60 * 1000);
|
setInterval(runHijackCheck, 30 * 60 * 1000);
|
||||||
@ -4987,7 +5090,152 @@ ${html}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hijack Subscribe ──────────────────────────────────────────
|
// ── CORS preflight for all /api/webhooks + /api/hijacks ──────
|
||||||
|
if ((reqPath.startsWith('/api/webhooks') || reqPath.startsWith('/api/hijacks') || reqPath === '/api/hijack-subscribe') && req.method === 'OPTIONS') {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||||
|
res.writeHead(204); return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Webhook: Register ─────────────────────────────────────────
|
||||||
|
// POST /api/webhooks?asn=X body: { endpoint_url, timeout_ms?, max_retries? }
|
||||||
|
if (reqPath === '/api/webhooks' && req.method === 'POST') {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
||||||
|
const asn = params.get('asn') ? String(params.get('asn')).replace(/[^0-9]/g,'') : '';
|
||||||
|
if (!asn) { res.writeHead(400); return res.end(JSON.stringify({error:'asn query param required'})); }
|
||||||
|
let body = '';
|
||||||
|
req.on('data', c => body += c);
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
const { endpoint_url, timeout_ms, max_retries } = JSON.parse(body || '{}');
|
||||||
|
if (!endpoint_url || !endpoint_url.startsWith('http')) {
|
||||||
|
res.writeHead(400); return res.end(JSON.stringify({error:'endpoint_url required (http/https)'}));
|
||||||
|
}
|
||||||
|
const subs = loadWebhookSubs();
|
||||||
|
const exists = subs.find(s => s.asn === asn && s.endpoint_url === endpoint_url);
|
||||||
|
if (exists) { res.writeHead(200); return res.end(JSON.stringify({id: exists.id, already_exists: true, secret_key: exists.secret})); }
|
||||||
|
const secret = crypto.randomBytes(32).toString('hex');
|
||||||
|
const entry = {
|
||||||
|
id: crypto.randomBytes(8).toString('hex'),
|
||||||
|
asn, endpoint_url,
|
||||||
|
secret,
|
||||||
|
timeout_ms: timeout_ms || 10000,
|
||||||
|
max_retries: max_retries || 5,
|
||||||
|
active: true,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
last_triggered_at: null,
|
||||||
|
failure_count: 0
|
||||||
|
};
|
||||||
|
subs.push(entry);
|
||||||
|
saveWebhookSubs(subs);
|
||||||
|
// Also ensure this ASN is in hijack monitoring
|
||||||
|
const hijackSubs = loadHijackSubs();
|
||||||
|
if (!hijackSubs.find(s => s.asn === asn)) {
|
||||||
|
checkHijacksForAsn(asn).then(prefixes => {
|
||||||
|
hijackSubs.push({ asn, prefixes, subscribed: new Date().toISOString() });
|
||||||
|
saveHijackSubs(hijackSubs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.writeHead(201);
|
||||||
|
res.end(JSON.stringify({ id: entry.id, asn, endpoint_url, secret_key: secret, created_at: entry.created_at, note: 'Store secret_key safely — it signs all webhook payloads' }));
|
||||||
|
} catch(e) { res.writeHead(400); res.end(JSON.stringify({error: e.message})); }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Webhook: List ─────────────────────────────────────────────
|
||||||
|
// GET /api/webhooks?asn=X
|
||||||
|
if (reqPath === '/api/webhooks' && req.method === 'GET') {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
||||||
|
const asn = params.get('asn') ? String(params.get('asn')).replace(/[^0-9]/g,'') : '';
|
||||||
|
const subs = loadWebhookSubs();
|
||||||
|
const result = (asn ? subs.filter(s => s.asn === asn) : subs)
|
||||||
|
.map(s => ({ id: s.id, asn: s.asn, endpoint_url: s.endpoint_url, active: s.active, failure_count: s.failure_count, last_triggered_at: s.last_triggered_at, created_at: s.created_at }));
|
||||||
|
res.writeHead(200);
|
||||||
|
return res.end(JSON.stringify({ webhooks: result, total: result.length }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Webhook: Delete ───────────────────────────────────────────
|
||||||
|
// DELETE /api/webhooks/:id
|
||||||
|
if (reqPath.startsWith('/api/webhooks/') && req.method === 'DELETE' && !reqPath.includes('/test')) {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
const id = reqPath.split('/')[3];
|
||||||
|
const subs = loadWebhookSubs();
|
||||||
|
const idx = subs.findIndex(s => s.id === id);
|
||||||
|
if (idx === -1) { res.writeHead(404); return res.end(JSON.stringify({error:'webhook not found'})); }
|
||||||
|
subs.splice(idx, 1);
|
||||||
|
saveWebhookSubs(subs);
|
||||||
|
res.writeHead(200);
|
||||||
|
return res.end(JSON.stringify({ deleted: true, id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Webhook: Test ─────────────────────────────────────────────
|
||||||
|
// POST /api/webhooks/:id/test
|
||||||
|
if (reqPath.match(/^\/api\/webhooks\/[^/]+\/test$/) && req.method === 'POST') {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
const id = reqPath.split('/')[3];
|
||||||
|
const subs = loadWebhookSubs();
|
||||||
|
const sub = subs.find(s => s.id === id);
|
||||||
|
if (!sub) { res.writeHead(404); return res.end(JSON.stringify({error:'webhook not found'})); }
|
||||||
|
const testPayload = { event: 'test', asn: sub.asn, message: 'PeerCortex webhook test — if you receive this, delivery works!', timestamp: new Date().toISOString() };
|
||||||
|
const start = Date.now();
|
||||||
|
deliverWebhook(sub, testPayload, 1).then(result => {
|
||||||
|
res.writeHead(result.ok ? 200 : 502);
|
||||||
|
res.end(JSON.stringify({ ok: result.ok, response_time_ms: Date.now() - start, status: result.status, error: result.error || null }));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hijack Events: List ───────────────────────────────────────
|
||||||
|
// GET /api/hijacks?asn=X&limit=50&resolved=false
|
||||||
|
if (reqPath === '/api/hijacks' && req.method === 'GET') {
|
||||||
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
||||||
|
const asn = params.get('asn') ? String(params.get('asn')).replace(/[^0-9]/g,'') : '';
|
||||||
|
const limit = Math.min(parseInt(params.get('limit') || '50', 10), 200);
|
||||||
|
const resolvedFilter = params.get('resolved');
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Cache-Control', 'no-store');
|
||||||
|
let events = loadHijackAlerts();
|
||||||
|
if (asn) events = events.filter(a => String(a.asn) === asn);
|
||||||
|
if (resolvedFilter === 'false') events = events.filter(a => !a.resolved);
|
||||||
|
if (resolvedFilter === 'true') events = events.filter(a => a.resolved);
|
||||||
|
const total = events.length;
|
||||||
|
events = events.slice(-limit).reverse();
|
||||||
|
res.writeHead(200);
|
||||||
|
return res.end(JSON.stringify({ total, events }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hijack Events: Resolve ────────────────────────────────────
|
||||||
|
// POST /api/hijacks/:id/resolve body: { resolution_notes? }
|
||||||
|
if (reqPath.match(/^\/api\/hijacks\/[^/]+\/resolve$/) && req.method === 'POST') {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
const id = reqPath.split('/')[3];
|
||||||
|
let body = '';
|
||||||
|
req.on('data', c => body += c);
|
||||||
|
req.on('end', () => {
|
||||||
|
const alerts = loadHijackAlerts();
|
||||||
|
const alert = alerts.find(a => a.id === id);
|
||||||
|
if (!alert) { res.writeHead(404); return res.end(JSON.stringify({error:'event not found'})); }
|
||||||
|
alert.resolved = true;
|
||||||
|
alert.resolved_at = new Date().toISOString();
|
||||||
|
try { const { resolution_notes } = JSON.parse(body || '{}'); if (resolution_notes) alert.resolution_notes = resolution_notes; } catch(_){}
|
||||||
|
saveHijackAlerts(alerts);
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({ resolved: true, id, resolved_at: alert.resolved_at }));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hijack Subscribe (legacy, kept for backwards compatibility) ─
|
||||||
if (reqPath === '/api/hijack-subscribe' && req.method === 'POST') {
|
if (reqPath === '/api/hijack-subscribe' && req.method === 'POST') {
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
@ -4995,30 +5243,25 @@ ${html}
|
|||||||
req.on('data', c => body += c);
|
req.on('data', c => body += c);
|
||||||
req.on('end', async () => {
|
req.on('end', async () => {
|
||||||
try {
|
try {
|
||||||
const { asn, email } = JSON.parse(body);
|
const { asn } = JSON.parse(body || '{}');
|
||||||
const asnNum = String(asn).replace(/[^0-9]/g,'');
|
const asnNum = String(asn || '').replace(/[^0-9]/g,'');
|
||||||
if (!asnNum) { res.writeHead(400); return res.end(JSON.stringify({error:'asn required'})); }
|
if (!asnNum) { res.writeHead(400); return res.end(JSON.stringify({error:'asn required'})); }
|
||||||
const subs = loadHijackSubs();
|
const subs = loadHijackSubs();
|
||||||
const exists = subs.find(s => s.asn === asnNum);
|
const exists = subs.find(s => s.asn === asnNum);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
const prefixes = await checkHijacksForAsn(asnNum);
|
const prefixes = await checkHijacksForAsn(asnNum);
|
||||||
subs.push({ asn: asnNum, email: email || '', prefixes, subscribed: new Date().toISOString() });
|
subs.push({ asn: asnNum, prefixes, subscribed: new Date().toISOString() });
|
||||||
fs.writeFileSync(HIJACK_SUBS_FILE, JSON.stringify(subs, null, 2));
|
saveHijackSubs(subs);
|
||||||
}
|
}
|
||||||
|
const sub = subs.find(s => s.asn === asnNum) || { prefixes: [] };
|
||||||
res.writeHead(200);
|
res.writeHead(200);
|
||||||
res.end(JSON.stringify({ ok: true, asn: asnNum, monitoring: true, prefix_count: exists ? exists.prefixes.length : subs[subs.length-1].prefixes.length }));
|
res.end(JSON.stringify({ ok: true, asn: asnNum, monitoring: true, prefix_count: sub.prefixes.length }));
|
||||||
} catch(e) { res.writeHead(500); res.end(JSON.stringify({error:e.message})); }
|
} catch(e) { res.writeHead(500); res.end(JSON.stringify({error:e.message})); }
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (reqPath === '/api/hijack-subscribe' && req.method === 'OPTIONS') {
|
|
||||||
res.setHeader('Access-Control-Allow-Origin','*');
|
|
||||||
res.setHeader('Access-Control-Allow-Methods','POST,OPTIONS');
|
|
||||||
res.setHeader('Access-Control-Allow-Headers','Content-Type');
|
|
||||||
res.writeHead(204); return res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Hijack Alerts ─────────────────────────────────────────────
|
// ── Hijack Alerts (legacy endpoint, kept for backwards compatibility) ─
|
||||||
if (reqPath.startsWith('/api/hijack-alerts')) {
|
if (reqPath.startsWith('/api/hijack-alerts')) {
|
||||||
const params = new URL(req.url, 'http://localhost').searchParams;
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
||||||
const asn = (params.get('asn') || '').replace(/[^0-9]/g,'');
|
const asn = (params.get('asn') || '').replace(/[^0-9]/g,'');
|
||||||
@ -5026,7 +5269,7 @@ ${html}
|
|||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
res.setHeader('Cache-Control', 'no-store');
|
res.setHeader('Cache-Control', 'no-store');
|
||||||
const allAlerts = loadHijackAlerts();
|
const allAlerts = loadHijackAlerts();
|
||||||
const alerts = asn ? allAlerts.filter(a => a.asn === asn) : allAlerts;
|
const alerts = asn ? allAlerts.filter(a => String(a.asn) === asn) : allAlerts;
|
||||||
const subs = loadHijackSubs();
|
const subs = loadHijackSubs();
|
||||||
const sub = subs.find(s => s.asn === asn);
|
const sub = subs.find(s => s.asn === asn);
|
||||||
res.writeHead(200);
|
res.writeHead(200);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user