mirror of
https://github.com/MrUnknownDE/utools.git
synced 2026-05-06 06:16:04 +02:00
350 lines
19 KiB
JavaScript
350 lines
19 KiB
JavaScript
import { API } from '../shared.js';
|
|
|
|
export const page = {
|
|
title: 'ASN Lookup',
|
|
|
|
template: () => `
|
|
<div class="container mx-auto max-w-6xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
|
|
<h1 class="text-3xl font-bold mb-2 text-center text-gradient">AS / ASN Lookup</h1>
|
|
<p class="text-center text-gray-400 text-sm mb-8">Peering graph, prefixes & IXP connections for any Autonomous System</p>
|
|
|
|
<div class="flex flex-col sm:flex-row gap-3 mb-6 max-w-2xl mx-auto">
|
|
<input type="text" id="asn-input" placeholder="Enter ASN (e.g. 15169 or AS3320)"
|
|
class="flex-grow px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all placeholder-gray-600">
|
|
<button id="lookup-button" disabled
|
|
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-bold py-3 px-6 rounded-lg shadow-lg transition-all duration-200">
|
|
Lookup
|
|
</button>
|
|
</div>
|
|
|
|
<div id="error-box" class="hidden max-w-2xl mx-auto mb-6 p-4 bg-red-900/30 border border-red-500/40 text-red-300 rounded-lg text-sm"></div>
|
|
|
|
<div id="loading-section" class="hidden flex flex-col items-center gap-3 py-16">
|
|
<div class="loader" style="width:40px;height:40px;border-width:5px;"></div>
|
|
<p class="text-gray-400 text-sm" id="loading-msg">Querying RIPE Stat & PeeringDB…</p>
|
|
<p class="text-xs text-amber-400/80 bg-amber-400/10 border border-amber-400/20 rounded-lg px-4 py-2 max-w-sm text-center mt-1">
|
|
⏳ Large ASes (Cloudflare, Google, Tier-1 carriers) can take up to 15 s on first lookup — subsequent lookups are cached for 7 days.
|
|
</p>
|
|
</div>
|
|
|
|
<div id="results-section" class="hidden fade-in">
|
|
<div class="glass-card rounded-xl p-6 mb-6">
|
|
<div class="flex flex-col md:flex-row md:items-center gap-4">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-1">
|
|
<span id="res-asn" class="font-mono text-2xl font-bold text-purple-400"></span>
|
|
<span id="res-announced-badge" class="hidden text-xs px-2 py-0.5 bg-green-500/20 border border-green-500/40 text-green-400 rounded-full">Announced</span>
|
|
<span id="res-type-badge" class="text-xs px-2 py-0.5 bg-blue-500/20 border border-blue-500/40 text-blue-300 rounded-full"></span>
|
|
</div>
|
|
<h2 id="res-name" class="text-xl font-semibold text-white mb-1"></h2>
|
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-400 mb-2">
|
|
<span id="res-policy-container" class="hidden">Peering Policy: <span id="res-policy" class="text-gray-200"></span></span>
|
|
<span id="res-website-container" class="hidden">Website: <a id="res-website" href="#" target="_blank" rel="noopener" class="text-purple-400 hover:text-purple-300 transition-colors"></a></span>
|
|
</div>
|
|
<div id="res-rich-info" class="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-4 pt-4 border-t border-gray-700/50 hidden">
|
|
<div id="res-info-type-container" class="hidden"><div class="text-xs text-gray-500 uppercase tracking-widest">Type</div><div id="res-info-type" class="text-sm text-gray-200 capitalize mt-0.5"></div></div>
|
|
<div id="res-info-scope-container" class="hidden"><div class="text-xs text-gray-500 uppercase tracking-widest">Scope</div><div id="res-info-scope" class="text-sm text-gray-200 capitalize mt-0.5"></div></div>
|
|
<div id="res-info-traffic-container" class="hidden"><div class="text-xs text-gray-500 uppercase tracking-widest">Traffic</div><div id="res-info-traffic" class="text-sm text-gray-200 capitalize mt-0.5"></div></div>
|
|
<div id="res-info-ratio-container" class="hidden"><div class="text-xs text-gray-500 uppercase tracking-widest">Ratio</div><div id="res-info-ratio" class="text-sm text-gray-200 capitalize mt-0.5"></div></div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 text-center">
|
|
<div class="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3">
|
|
<div id="res-upstream-count" class="text-xl font-bold text-blue-400">—</div>
|
|
<div class="text-xs text-gray-400 mt-0.5">Upstreams</div>
|
|
</div>
|
|
<div class="bg-green-500/10 border border-green-500/20 rounded-lg p-3">
|
|
<div id="res-downstream-count" class="text-xl font-bold text-green-400">—</div>
|
|
<div class="text-xs text-gray-400 mt-0.5">Downstreams</div>
|
|
</div>
|
|
<div class="bg-purple-500/10 border border-purple-500/20 rounded-lg p-3 col-span-2 sm:col-span-1">
|
|
<div id="res-prefix-count" class="text-xl font-bold text-purple-400">—</div>
|
|
<div class="text-xs text-gray-400 mt-0.5">Prefixes</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="glass-card rounded-xl p-6 mb-6">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<h3 class="text-lg font-bold text-purple-300">Network Map</h3>
|
|
<div class="flex gap-3 text-xs text-gray-400">
|
|
<span class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded-full bg-gray-500"></span>Tier-1</span>
|
|
<span class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded-full bg-blue-500"></span>Upstream</span>
|
|
<span class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded-full bg-purple-500"></span>This AS</span>
|
|
<span class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded-full bg-green-500"></span>Downstream</span>
|
|
</div>
|
|
<span class="ml-auto text-xs text-gray-500">Scroll to zoom · Drag to pan · Click node to open</span>
|
|
</div>
|
|
<div id="graph-container">
|
|
<svg id="graph-svg"></svg>
|
|
<div id="graph-tooltip"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
<div class="glass-card rounded-xl p-5">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest">Announced Prefixes</h3>
|
|
<button id="prefix-toggle" class="text-xs text-purple-400 hover:text-purple-300 transition-colors">Show all</button>
|
|
</div>
|
|
<div id="prefix-list" class="max-h-48 overflow-y-auto"></div>
|
|
<p id="prefix-empty" class="hidden text-sm text-gray-500 italic">No prefix data available.</p>
|
|
</div>
|
|
<div class="glass-card rounded-xl p-5">
|
|
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-3">IXP Presence <span class="text-xs font-normal text-gray-500">(via PeeringDB)</span></h3>
|
|
<div id="ixp-list" class="space-y-1 text-sm max-h-48 overflow-y-auto">
|
|
<p id="ixp-empty" class="text-gray-500 italic text-sm">Not listed on PeeringDB.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="glass-card rounded-xl p-5">
|
|
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-3">Direct Neighbours <span class="text-xs font-normal text-gray-500">(Level 2 · via RIPE Stat)</span></h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<h4 class="text-xs font-semibold text-blue-400 mb-2">↑ Upstreams (Transit Providers)</h4>
|
|
<div id="upstream-table" class="space-y-1 text-xs font-mono max-h-52 overflow-y-auto"></div>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-xs font-semibold text-green-400 mb-2">↓ Downstreams (Customers)</h4>
|
|
<div id="downstream-table" class="space-y-1 text-xs font-mono max-h-52 overflow-y-auto"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`,
|
|
|
|
async init(search) {
|
|
const asnInput = document.getElementById('asn-input');
|
|
const lookupButton = document.getElementById('lookup-button');
|
|
const errorBox = document.getElementById('error-box');
|
|
const loadingSection = document.getElementById('loading-section');
|
|
const resultsSection = document.getElementById('results-section');
|
|
|
|
let currentData = null;
|
|
let showAllPrefixes = false;
|
|
|
|
const syncBtn = () => { lookupButton.disabled = !asnInput.value.trim(); };
|
|
asnInput.addEventListener('input', syncBtn);
|
|
|
|
function showError(msg) {
|
|
errorBox.textContent = msg;
|
|
errorBox.classList.toggle('hidden', !msg);
|
|
loadingSection.classList.add('hidden');
|
|
resultsSection.classList.add('hidden');
|
|
}
|
|
|
|
function setLoading(msg) {
|
|
errorBox.classList.add('hidden');
|
|
document.getElementById('loading-msg').textContent = msg;
|
|
loadingSection.classList.remove('hidden');
|
|
resultsSection.classList.add('hidden');
|
|
}
|
|
|
|
async function doLookup(rawAsn) {
|
|
const asn = String(rawAsn || '').trim().toUpperCase().replace(/^AS/, '');
|
|
if (!asn || isNaN(Number(asn))) { showError('Please enter a valid ASN (e.g. 15169 or AS3320).'); return; }
|
|
|
|
setLoading('Querying RIPE Stat & PeeringDB…');
|
|
const url = new URL(location.href);
|
|
url.searchParams.set('asn', asn);
|
|
history.replaceState({}, '', url);
|
|
|
|
try {
|
|
const res = await fetch(`${API}/asn-lookup?asn=${encodeURIComponent(asn)}`);
|
|
const data = await res.json();
|
|
if (!res.ok || !data.success) { showError(data.error || `Request failed (HTTP ${res.status})`); return; }
|
|
currentData = data;
|
|
renderResults(data);
|
|
} catch (err) {
|
|
showError(`Network error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
function renderResults(data) {
|
|
loadingSection.classList.add('hidden');
|
|
resultsSection.classList.remove('hidden');
|
|
|
|
document.getElementById('res-asn').textContent = `AS${data.asn}`;
|
|
document.getElementById('res-name').textContent = data.name || 'Unknown';
|
|
|
|
const announcedBadge = document.getElementById('res-announced-badge');
|
|
announcedBadge.classList.toggle('hidden', !data.announced);
|
|
|
|
const typeBadge = document.getElementById('res-type-badge');
|
|
typeBadge.textContent = data.type || '';
|
|
typeBadge.classList.toggle('hidden', !data.type);
|
|
|
|
const policyContainer = document.getElementById('res-policy-container');
|
|
const policyEl = document.getElementById('res-policy');
|
|
if (data.peeringdb?.peeringPolicy) {
|
|
policyEl.textContent = data.peeringdb.peeringPolicy;
|
|
policyContainer.classList.remove('hidden');
|
|
} else { policyContainer.classList.add('hidden'); }
|
|
|
|
const websiteContainer = document.getElementById('res-website-container');
|
|
const websiteEl = document.getElementById('res-website');
|
|
if (data.peeringdb?.website) {
|
|
websiteEl.href = data.peeringdb.website;
|
|
websiteEl.textContent = data.peeringdb.website.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
websiteContainer.classList.remove('hidden');
|
|
} else { websiteContainer.classList.add('hidden'); }
|
|
|
|
const richInfo = document.getElementById('res-rich-info');
|
|
let hasRich = false;
|
|
[['type', data.peeringdb?.infoType], ['scope', data.peeringdb?.infoScope],
|
|
['traffic', data.peeringdb?.infoTraffic], ['ratio', data.peeringdb?.infoRatio]].forEach(([id, val]) => {
|
|
const c = document.getElementById(`res-info-${id}-container`);
|
|
const e = document.getElementById(`res-info-${id}`);
|
|
if (c && e) { if (val) { e.textContent = val; c.classList.remove('hidden'); hasRich = true; } else c.classList.add('hidden'); }
|
|
});
|
|
richInfo.classList.toggle('hidden', !hasRich);
|
|
|
|
document.getElementById('res-upstream-count').textContent = data.graph?.level2?.upstreams?.length ?? '?';
|
|
document.getElementById('res-downstream-count').textContent = data.graph?.level2?.downstreams?.length ?? '?';
|
|
document.getElementById('res-prefix-count').textContent = data.prefixes?.length ?? '?';
|
|
|
|
renderPrefixes(data.prefixes);
|
|
renderIxps(data.peeringdb?.ixps);
|
|
renderNeighbourTable('upstream-table', data.graph?.level2?.upstreams ?? [], 'blue');
|
|
renderNeighbourTable('downstream-table', data.graph?.level2?.downstreams ?? [], 'green');
|
|
if (data.graph) renderGraph(data.graph);
|
|
}
|
|
|
|
function renderPrefixes(prefixes) {
|
|
const list = document.getElementById('prefix-list');
|
|
const empty = document.getElementById('prefix-empty');
|
|
const toggle = document.getElementById('prefix-toggle');
|
|
if (!prefixes?.length) { list.classList.add('hidden'); empty.classList.remove('hidden'); toggle.classList.add('hidden'); return; }
|
|
empty.classList.add('hidden'); toggle.classList.remove('hidden');
|
|
const toShow = showAllPrefixes ? prefixes : prefixes.slice(0, 20);
|
|
list.innerHTML = toShow.map(p => `<span class="prefix-tag">${p}</span>`).join('');
|
|
toggle.textContent = showAllPrefixes ? 'Show less' : `Show all (${prefixes.length})`;
|
|
}
|
|
|
|
document.getElementById('prefix-toggle').addEventListener('click', () => {
|
|
showAllPrefixes = !showAllPrefixes;
|
|
if (currentData) renderPrefixes(currentData.prefixes);
|
|
});
|
|
|
|
function renderIxps(ixps) {
|
|
const list = document.getElementById('ixp-list');
|
|
const empty = document.getElementById('ixp-empty');
|
|
if (!ixps?.length) { list.innerHTML = ''; empty.classList.remove('hidden'); return; }
|
|
empty.classList.add('hidden');
|
|
list.innerHTML = ixps.map(ix => `
|
|
<div class="ixp-row py-1.5 flex items-center justify-between gap-2 text-sm">
|
|
<span class="text-gray-200 truncate">${ix.name}</span>
|
|
<span class="text-xs text-gray-500 shrink-0">${ix.speed >= 1000 ? ix.speed / 1000 + 'G' : ix.speed + 'M'}</span>
|
|
</div>`).join('');
|
|
}
|
|
|
|
function renderNeighbourTable(elId, nodes, colour) {
|
|
const el = document.getElementById(elId);
|
|
if (!nodes?.length) { el.innerHTML = '<p class="text-gray-500 italic">None reported.</p>'; return; }
|
|
const col = colour === 'blue' ? 'text-blue-400' : 'text-green-400';
|
|
el.innerHTML = nodes.map(n => `
|
|
<div class="flex items-center gap-2 py-0.5 hover:bg-white/5 rounded px-1 cursor-pointer group"
|
|
onclick="window._router.navigate('/asn',{asn:'${n.asn}'})">
|
|
<span class="${col} font-bold w-14 shrink-0">AS${n.asn}</span>
|
|
<span class="text-gray-300 truncate flex-1 group-hover:text-white">${n.name || '—'}</span>
|
|
<span class="text-gray-600 shrink-0">${n.power ? 'pwr:' + n.power : ''}</span>
|
|
</div>`).join('');
|
|
}
|
|
|
|
function renderGraph(graph) {
|
|
const container = document.getElementById('graph-container');
|
|
const svg = d3.select('#graph-svg');
|
|
svg.selectAll('*').remove();
|
|
const W = container.clientWidth, H = container.clientHeight;
|
|
|
|
const nodeMap = new Map();
|
|
function addNode(asn, name, role) {
|
|
const key = String(asn);
|
|
if (!nodeMap.has(key)) nodeMap.set(key, { id: key, asn, name: name || `AS${asn}`, role });
|
|
}
|
|
addNode(graph.center.asn, graph.center.name, 'center');
|
|
const vizUp = graph.level2.upstreams.slice(0, 15);
|
|
const vizDown = graph.level2.downstreams.slice(0, 15);
|
|
vizUp.forEach(n => addNode(n.asn, n.name, 'upstream'));
|
|
vizDown.forEach(n => addNode(n.asn, n.name, 'downstream'));
|
|
graph.level3.forEach(d => d.upstreams.forEach(n => addNode(n.asn, n.name, 'tier1')));
|
|
|
|
const nodes = Array.from(nodeMap.values());
|
|
const links = [];
|
|
const cid = String(graph.center.asn);
|
|
vizUp.forEach(n => links.push({ source: String(n.asn), target: cid, type: 'upstream', power: n.power || 1 }));
|
|
vizDown.forEach(n => links.push({ source: cid, target: String(n.asn), type: 'downstream', power: n.power || 1 }));
|
|
graph.level3.forEach(d => d.upstreams.forEach(n => links.push({ source: String(n.asn), target: String(d.parentAsn), type: 'tier1', power: n.power || 1 })));
|
|
const uniqueLinks = Array.from(new Map(links.map(l => [`${l.source}-${l.target}`, l])).values());
|
|
|
|
const layerX = { tier1: W * 0.08, upstream: W * 0.3, center: W * 0.55, downstream: W * 0.8 };
|
|
nodes.forEach(n => { n.fx = layerX[n.role] ?? W / 2; });
|
|
|
|
const maxPow = Math.max(...uniqueLinks.map(l => l.power), 1);
|
|
const strokeSc = d3.scaleLinear().domain([0, maxPow]).range([0.5, 4]);
|
|
const nodeRadius = { center: 20, upstream: 11, downstream: 11, tier1: 8 };
|
|
|
|
const sim = d3.forceSimulation(nodes)
|
|
.force('link', d3.forceLink(uniqueLinks).id(d => d.id).distance(d => d.type === 'tier1' ? 90 : 120).strength(0.6))
|
|
.force('charge', d3.forceManyBody().strength(-220))
|
|
.force('y', d3.forceY(H / 2).strength(0.04))
|
|
.force('collide', d3.forceCollide().radius(d => nodeRadius[d.role] + 14))
|
|
.alphaDecay(0.025);
|
|
|
|
const g = svg.append('g');
|
|
svg.call(d3.zoom().scaleExtent([0.3, 3]).on('zoom', evt => g.attr('transform', evt.transform)));
|
|
|
|
const link = g.append('g').selectAll('line').data(uniqueLinks).join('line')
|
|
.attr('class', d => `link link-${d.type}`)
|
|
.attr('stroke-width', d => strokeSc(d.power));
|
|
|
|
const tooltip = document.getElementById('graph-tooltip');
|
|
const node = g.append('g').selectAll('g').data(nodes).join('g')
|
|
.attr('class', d => `node node-${d.role}`)
|
|
.style('cursor', 'pointer')
|
|
.on('click', (_, d) => { if (d.role !== 'center') window._router.navigate('/asn', { asn: d.asn }); })
|
|
.on('mouseenter', (_, d) => {
|
|
tooltip.style.opacity = '1';
|
|
tooltip.innerHTML = `<strong class="text-purple-300">AS${d.asn}</strong><br><span class="text-gray-300">${d.name}</span><br><span class="text-gray-500 text-xs capitalize">${d.role === 'tier1' ? 'Tier-1 / Transit' : d.role}</span>`;
|
|
})
|
|
.on('mousemove', evt => {
|
|
const r = container.getBoundingClientRect();
|
|
let x = evt.clientX - r.left + 14, y = evt.clientY - r.top - 10;
|
|
if (x + 230 > W) x -= 244;
|
|
tooltip.style.left = x + 'px'; tooltip.style.top = y + 'px';
|
|
})
|
|
.on('mouseleave', () => { tooltip.style.opacity = '0'; })
|
|
.call(d3.drag()
|
|
.on('start', (evt, d) => { if (!evt.active) sim.alphaTarget(0.3).restart(); d.fy = d.y; })
|
|
.on('drag', (evt, d) => { d.fy = evt.y; })
|
|
.on('end', (evt, d) => { if (!evt.active) sim.alphaTarget(0); d.fy = null; }));
|
|
|
|
node.append('circle').attr('r', d => nodeRadius[d.role]);
|
|
node.append('text').attr('dy', d => nodeRadius[d.role] + 13).attr('font-size', d => d.role === 'center' ? 12 : 9).text(d => `AS${d.asn}`);
|
|
node.append('text').attr('dy', d => nodeRadius[d.role] + 23)
|
|
.attr('font-size', d => d.role === 'tier1' ? 7 : 8)
|
|
.attr('fill', d => d.role === 'tier1' ? '#6b7280' : '#9ca3af')
|
|
.text(d => {
|
|
if (!d.name) return '';
|
|
const max = d.role === 'center' ? 22 : d.role === 'tier1' ? 12 : 16;
|
|
return d.name.length > max ? d.name.slice(0, max) + '…' : d.name;
|
|
});
|
|
|
|
sim.on('tick', () => {
|
|
nodes.forEach(n => { n.y = Math.max(30, Math.min(H - 30, n.y)); });
|
|
link.attr('x1', d => d.source.x).attr('y1', d => d.source.y).attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
node.attr('transform', d => `translate(${d.x},${d.y})`);
|
|
});
|
|
}
|
|
|
|
lookupButton.addEventListener('click', () => doLookup(asnInput.value));
|
|
asnInput.addEventListener('keypress', e => { if (e.key === 'Enter' && !lookupButton.disabled) doLookup(asnInput.value); });
|
|
|
|
const params = new URLSearchParams(search);
|
|
const urlAsn = params.get('asn');
|
|
if (urlAsn) { asnInput.value = urlAsn; syncBtn(); doLookup(urlAsn); }
|
|
}
|
|
};
|