server/routes/static.js (/, /index.html, /index-editorial.html, /favicon.ico, /lia), server/routes/search.js (/api/search, /api/visitors), server/routes/feedback.js (/api/feedback OPTIONS/POST/GET). Verified via node --check, eslint no-undef (only the 3 known pre-existing bugs remain, no new ones), and smoke-test harness (28/28 match). server.js: 3714 -> 3556 lines.
84 lines
3.4 KiB
JavaScript
84 lines
3.4 KiB
JavaScript
const { fetchJSONWithRetry } = require("../services/http-helpers");
|
|
const { PEERINGDB_API_KEY } = require("../services/peeringdb");
|
|
const { trackVisitor } = require("../visitors");
|
|
|
|
// Name Search (RIPE Stat + PeeringDB combined)
|
|
async function handleSearch(req, res) {
|
|
const params = new URL(req.url, 'http://localhost').searchParams;
|
|
const q = (params.get('q') || '').trim();
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Cache-Control', 'public, max-age=120');
|
|
if (!q || q.length < 2) { res.writeHead(400); return res.end(JSON.stringify({error:'query too short'})); }
|
|
try {
|
|
const results = [];
|
|
const seen = new Set();
|
|
|
|
// Source 1: RIPE Stat searchcomplete (fast, covers ASNs + org names)
|
|
try {
|
|
const ripeUrl = 'https://stat.ripe.net/data/searchcomplete/data.json?resource=' + encodeURIComponent(q);
|
|
const ripeData = await fetchJSONWithRetry(ripeUrl, { timeout: 6000 });
|
|
const cats = ripeData && ripeData.data && ripeData.data.categories || [];
|
|
for (var ci = 0; ci < cats.length; ci++) {
|
|
var suggs = cats[ci].suggestions || [];
|
|
for (var si = 0; si < suggs.length; si++) {
|
|
var s = suggs[si];
|
|
var val = (s.value || '').toString();
|
|
// Only ASN results
|
|
if (/^AS\d+$/i.test(val) && !seen.has(val)) {
|
|
seen.add(val);
|
|
// Use description (e.g. "FLEXOPTIX, DE") as the display label
|
|
var ripeName = s.description || s.label || val;
|
|
results.push({ asn: val.replace(/^AS/i,''), label: ripeName, description: '', source: 'RIPE Stat' });
|
|
}
|
|
}
|
|
}
|
|
} catch(e) { /* RIPE Stat failed, continue */ }
|
|
|
|
// Source 2: PeeringDB name search (best for network operator names)
|
|
try {
|
|
var pdbUrl = 'https://www.peeringdb.com/api/net?name__icontains=' + encodeURIComponent(q) + '&depth=1&limit=10';
|
|
if (PEERINGDB_API_KEY) pdbUrl += '&key=' + PEERINGDB_API_KEY;
|
|
const pdbData = await fetchJSONWithRetry(pdbUrl, { timeout: 8000 });
|
|
var nets = pdbData && pdbData.data || [];
|
|
for (var ni = 0; ni < nets.length; ni++) {
|
|
var net = nets[ni];
|
|
var asnKey = 'AS' + net.asn;
|
|
if (net.asn && !seen.has(asnKey)) {
|
|
seen.add(asnKey);
|
|
var pdbDesc = [net.info_type, net.country].filter(Boolean).join(' · ');
|
|
results.push({
|
|
asn: String(net.asn),
|
|
label: net.name || asnKey,
|
|
description: pdbDesc,
|
|
source: 'PeeringDB'
|
|
});
|
|
}
|
|
}
|
|
} catch(e) { /* PeeringDB failed, continue */ }
|
|
|
|
// Sort: RIPE results first (usually more relevant for ASN lookup), then PeeringDB
|
|
results.sort((a, b) => {
|
|
if (a.source === b.source) return 0;
|
|
return a.source === 'RIPE Stat' ? -1 : 1;
|
|
});
|
|
|
|
res.writeHead(200);
|
|
return res.end(JSON.stringify({ q: q, results: results.slice(0, 12) }));
|
|
} catch(e) {
|
|
res.writeHead(500); return res.end(JSON.stringify({error: e.message}));
|
|
}
|
|
}
|
|
|
|
// GET /api/visitors — unique visitor count
|
|
function handleVisitors(req, res) {
|
|
res.setHeader("Content-Type", "application/json");
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
res.setHeader("Cache-Control", "no-store");
|
|
const count = trackVisitor(req);
|
|
res.writeHead(200);
|
|
return res.end(JSON.stringify({ visitors: count }));
|
|
}
|
|
|
|
module.exports = { handleSearch, handleVisitors };
|