diff --git a/deploy/public/lia.html b/deploy/public/lia.html index 9deb108..64d49ab 100644 --- a/deploy/public/lia.html +++ b/deploy/public/lia.html @@ -80,6 +80,22 @@ body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background: +
+
+
+
FILE LOOKUP
+
Upload a file with ASNs or company names — we'll check probe coverage for each
+
+ + +
+ +
+
+
@@ -319,6 +335,152 @@ function exportPDF() { w.print(); } +// ============================================================ +// File Upload: Parse ASNs/company names from uploaded files +// ============================================================ +function handleFileUpload(input) { + var file = input.files[0]; + if (!file) return; + var status = $('fileStatus'); + status.textContent = 'Processing ' + file.name + '...'; + status.style.color = 'var(--cyan)'; + + var ext = file.name.split('.').pop().toLowerCase(); + + if (ext === 'csv' || ext === 'txt') { + file.text().then(function(text) { processFileText(text, file.name); }); + } else if (ext === 'pdf' || ext === 'doc' || ext === 'docx' || ext === 'xls' || ext === 'xlsx') { + // For binary formats, upload to server for parsing + var reader = new FileReader(); + reader.onload = function(e) { + var base64 = e.target.result.split(',')[1]; + fetch('/api/lia/parse-file', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename: file.name, data: base64 }) + }).then(function(r) { return r.json(); }).then(function(d) { + if (d.error) { + status.textContent = 'Error: ' + d.error; + status.style.color = 'var(--red)'; + return; + } + processFileText(d.text || '', file.name); + }).catch(function(err) { + // Fallback: try reading as text + file.text().then(function(text) { processFileText(text, file.name); }).catch(function() { + status.textContent = 'Cannot parse ' + ext.toUpperCase() + ' files client-side. Use CSV or TXT.'; + status.style.color = 'var(--red)'; + }); + }); + }; + reader.readAsDataURL(file); + } else { + status.textContent = 'Unsupported file type: ' + ext; + status.style.color = 'var(--red)'; + } +} + +function processFileText(text, filename) { + var status = $('fileStatus'); + + // Extract ASNs (AS12345 or just numbers that look like ASNs) + var asnMatches = text.match(/\bAS?(\d{3,7})\b/gi) || []; + var asns = new Set(); + asnMatches.forEach(function(m) { + var num = parseInt(m.replace(/^AS/i, '')); + if (num >= 100 && num <= 9999999) asns.add(num); + }); + + // Also try to match company names against our loaded data + var nameMatches = []; + if (allData.length > 0) { + var lines = text.split(/[\n\r,;]+/).map(function(l) { return l.trim().toLowerCase(); }).filter(function(l) { return l.length > 2; }); + lines.forEach(function(line) { + allData.forEach(function(n) { + if (n.name && n.name.toLowerCase().indexOf(line) >= 0 && line.length > 4) { + asns.add(n.asn); + } + if (line.indexOf(n.name ? n.name.toLowerCase() : '###') >= 0 && n.name && n.name.length > 4) { + asns.add(n.asn); + } + }); + }); + } + + if (asns.size === 0) { + status.textContent = 'No ASNs or matching networks found in ' + filename; + status.style.color = 'var(--orange)'; + return; + } + + // Match against our data + var results = []; + var asnArr = Array.from(asns); + var dataMap = {}; + allData.forEach(function(n) { dataMap[n.asn] = n; }); + + asnArr.forEach(function(asn) { + var n = dataMap[asn]; + results.push({ + asn: asn, + name: n ? n.name : '(Unknown)', + country: n ? n.country : '', + info_type: n ? n.info_type : '', + has_probe: n ? n.has_probe : false, + in_peeringdb: !!n, + }); + }); + + results.sort(function(a, b) { return (a.has_probe ? 1 : 0) - (b.has_probe ? 1 : 0); }); + + var withProbe = results.filter(function(r) { return r.has_probe; }).length; + var noProbe = results.length - withProbe; + + status.innerHTML = '' + withProbe + ' with probe · ' + noProbe + ' without probe · ' + results.length + ' total from ' + escHtml(filename); + + var h = '
'; + h += ''; + h += '
'; + h += '
'; + results.forEach(function(r) { + h += '
'; + h += 'AS' + r.asn + ''; + h += '' + escHtml(r.name) + ''; + if (r.country) h += '' + flag(r.country) + ' ' + r.country + ''; + if (r.info_type) h += '' + escHtml(r.info_type) + ''; + h += r.has_probe ? '\u2714 Probe' : '\u2718 No Probe'; + if (!r.in_peeringdb) h += '(not in PeeringDB)'; + h += '
'; + }); + h += '
'; + + $('fileResults').innerHTML = h; + $('fileResults').classList.remove('hidden'); + + // Store for export + window._fileResults = results; + window._fileName = filename; +} + +function exportFileResults() { + if (!window._fileResults) return; + var results = window._fileResults; + var w = window.open('', '_blank'); + var html = 'Atlas Probe Check — ' + escHtml(window._fileName) + ''; + html += ''; + html += '

Atlas Probe Coverage Check

'; + html += '
Source: ' + escHtml(window._fileName) + ' | Generated: ' + new Date().toISOString().split('T')[0] + ' | peercortex.org/lia
'; + html += ''; + results.forEach(function(r) { + html += ''; + html += ''; + }); + html += '
ASNNameCountryTypeAtlas Probe
AS' + r.asn + '' + escHtml(r.name) + '' + (r.country || '-') + '' + (r.info_type || '-') + '' + (r.has_probe ? 'YES' : 'NO') + '
'; + w.document.write(html); + w.document.close(); + w.print(); +} + // Boot loadData(); diff --git a/deploy/server.js b/deploy/server.js index 91f8fcf..650820a 100644 --- a/deploy/server.js +++ b/deploy/server.js @@ -737,101 +737,40 @@ const server = http.createServer(async (req, res) => { if (liaCached) return res.end(liaCached); // Fetch PeeringDB network list (all networks with status "ok") + // Use pre-cached org→country map (loaded at startup, 16MB response cached in memory) fetchPeeringDB("/net?status=ok&depth=0").then(function(pdbData) { if (!pdbData || !pdbData.data) { return res.end(JSON.stringify({ error: "Could not fetch PeeringDB networks" })); } var probeAsns = new Set(atlasProbeCache.asns_with_probes || []); - // Country name lookup - var countryNames = {}; - try { countryNames = require("./country-names.json"); } catch(_e) { /* optional */ } - var networks = pdbData.data.map(function(n) { + var enriched = pdbData.data.map(function(n) { + var org = pdbOrgCountryMap.get(n.org_id) || {}; + var cc = org.country || ""; return { asn: n.asn, name: n.name || "", - country: n.info_prefixes4 > 0 || n.info_prefixes6 > 0 ? "" : "", // PeeringDB doesn't have country directly on net + org_name: org.name || "", + country: cc, + country_name: cc, info_type: n.info_type || "", has_probe: probeAsns.has(n.asn), }; + }).filter(function(n) { return n.asn > 0 && n.country; }); + + var result = JSON.stringify({ + networks: enriched, + total: enriched.length, + with_probes: enriched.filter(function(n) { return n.has_probe; }).length, + without_probes: enriched.filter(function(n) { return !n.has_probe; }).length, + atlas_unique_asns: probeAsns.size, + org_countries_loaded: pdbOrgCountryMap.size, + fetched_at: new Date().toISOString(), }); - // We need countries — fetch from RIPE Stat for each unique ASN is too slow. - // Instead, use the Atlas byCountry data to enrich. For PeeringDB, we need netfac or netixlan for country. - // Better approach: Use PeeringDB org country. Fetch with depth=1 to get org. - // But that's too heavy (50MB+). Instead, use a separate PeeringDB call for orgs. - // Pragmatic: Fetch net with depth=1 but limit fields - // Actually the simplest: PeeringDB net API doesn't expose country directly. - // Use the ix_count/fac_count fields and the "org" for country. - // Let's just add a second call for orgs. - - // Simplest approach: use RIPE Stat resource-overview for country from ASN prefix - // But that's per-ASN. Instead, build country from Atlas probes data. - // Atlas byCountry has {CC: {asnSet}} — we can reverse-map ASN→country from there. - - // Build ASN→country from atlas data - // We stored asnSet in byCountry but only in the internal function. - // atlasProbeCache.by_country has {CC: {total, connected, asn_count}} — no ASN list! - // We need to store ASN→country mapping. Let's add it. - - // For now, return without country and let frontend handle it via RIR mapping - // Actually: PeeringDB net objects have no country, but we can batch-fetch orgs. - // The org object has country. Let's do net?depth=1 but that's huge. - // Compromise: Get first 5000 networks and their org_id, then batch-fetch orgs. - - // PRAGMATIC FIX: Use net?depth=0 + a separate org fetch - // PeeringDB org API: /org?status=ok&limit=0 returns all orgs with country. - - fetchPeeringDB("/org?status=ok&depth=0").then(function(orgData) { - // Build org_id → country map - var orgCountry = {}; - if (orgData && orgData.data) { - orgData.data.forEach(function(o) { - orgCountry[o.id] = { country: o.country || "", name: o.name || "" }; - }); - } - - // Enrich networks with org country - var enriched = pdbData.data.map(function(n) { - var org = orgCountry[n.org_id] || {}; - var cc = org.country || ""; - return { - asn: n.asn, - name: n.name || "", - org_name: org.name || "", - country: cc, - country_name: cc, // frontend will display full name from its own mapping - info_type: n.info_type || "", - has_probe: probeAsns.has(n.asn), - }; - }).filter(function(n) { return n.asn > 0; }); - - var result = JSON.stringify({ - networks: enriched, - total: enriched.length, - with_probes: enriched.filter(function(n) { return n.has_probe; }).length, - without_probes: enriched.filter(function(n) { return !n.has_probe; }).length, - atlas_unique_asns: probeAsns.size, - fetched_at: new Date().toISOString(), - }); - - cacheSet(liaCacheKey, result, 30 * 60 * 1000); // 30 min cache - res.end(result); - }).catch(function(e) { - // If org fetch fails, return without country - var result = JSON.stringify({ - networks: networks, - total: networks.length, - with_probes: networks.filter(function(n) { return n.has_probe; }).length, - without_probes: networks.filter(function(n) { return !n.has_probe; }).length, - atlas_unique_asns: probeAsns.size, - error_note: "Country data unavailable: " + e.message, - fetched_at: new Date().toISOString(), - }); - cacheSet(liaCacheKey, result, 5 * 60 * 1000); - res.end(result); - }); + cacheSet(liaCacheKey, result, 30 * 60 * 1000); + res.end(result); }).catch(function(e) { res.end(JSON.stringify({ error: "PeeringDB fetch failed: " + e.message })); }); @@ -2447,10 +2386,59 @@ function fetchAllAtlasProbes() { }); } +// ============================================================ +// PeeringDB Org → Country Cache (for Lia's Paradise) +// ============================================================ +let pdbOrgCountryMap = new Map(); // org_id → { country, name } + +function fetchPdbOrgCountries() { + console.log("[PDB-ORG] Fetching PeeringDB org countries..."); + return new Promise(function(resolve) { + // Use raw https to handle the large 16MB response with streaming + var chunks = []; + var req = require("https").get("https://www.peeringdb.com/api/org?status=ok&depth=0", { + headers: { + "User-Agent": UA, + "Authorization": PEERINGDB_API_KEY ? "Api-Key " + PEERINGDB_API_KEY : undefined, + }, + timeout: 120000, + }, function(res) { + res.on("data", function(chunk) { chunks.push(chunk); }); + res.on("end", function() { + try { + var body = Buffer.concat(chunks).toString("utf8"); + var data = JSON.parse(body); + if (data && data.data) { + pdbOrgCountryMap = new Map(); + data.data.forEach(function(o) { + if (o.id && o.country) { + pdbOrgCountryMap.set(o.id, { country: o.country, name: o.name || "" }); + } + }); + console.log("[PDB-ORG] Loaded " + pdbOrgCountryMap.size + " org→country mappings"); + } + } catch (e) { + console.error("[PDB-ORG] Parse error:", e.message); + } + resolve(); + }); + }); + req.on("error", function(e) { + console.error("[PDB-ORG] Fetch error:", e.message); + resolve(); + }); + req.on("timeout", function() { + console.error("[PDB-ORG] Timeout after 120s"); + req.destroy(); + resolve(); + }); + }); +} + const PORT = process.env.PORT || 3101; // Fetch RPKI ASPA feed at startup and refresh every 10 minutes -Promise.all([fetchRpkiAspaFeed(), fetchAllAtlasProbes()]).then(() => { +Promise.all([fetchRpkiAspaFeed(), fetchAllAtlasProbes(), fetchPdbOrgCountries()]).then(() => { server.listen(PORT, "0.0.0.0", () => { console.log("PeerCortex v0.4.0 running on http://0.0.0.0:" + PORT); console.log("bgproutes.io API key: " + (BGPROUTES_API_KEY ? "configured" : "NOT configured"));