mirror of
https://github.com/MrUnknownDE/utools.git
synced 2026-05-06 06:16:04 +02:00
rebase www template
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
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); }
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { API, setupCopyBtn, showError } from '../shared.js';
|
||||
|
||||
export const page = {
|
||||
title: 'DNS Lookup',
|
||||
|
||||
template: () => `
|
||||
<div class="container mx-auto max-w-5xl 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-8 text-center text-gradient">DNS Lookup</h1>
|
||||
|
||||
<div class="p-6 glass-card rounded-xl">
|
||||
<div class="flex flex-col sm:flex-row gap-3 mb-6">
|
||||
<input type="text" id="dns-domain-input" placeholder="Enter domain (e.g., google.com)"
|
||||
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">
|
||||
<select id="dns-type-select"
|
||||
class="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 cursor-pointer">
|
||||
<option value="ANY">ANY</option><option value="A">A</option><option value="AAAA">AAAA</option>
|
||||
<option value="MX">MX</option><option value="TXT">TXT</option><option value="NS">NS</option>
|
||||
<option value="CNAME">CNAME</option><option value="SOA">SOA</option><option value="SRV">SRV</option>
|
||||
<option value="PTR">PTR (Reverse)</option>
|
||||
</select>
|
||||
<button id="dns-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 DNS
|
||||
</button>
|
||||
</div>
|
||||
<div id="dns-lookup-error" class="hidden mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-400 text-sm"></div>
|
||||
|
||||
<div id="dns-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-purple-300 flex items-center gap-2">
|
||||
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div>
|
||||
DNS Results for: <span id="dns-lookup-query" class="font-mono text-purple-400 ml-1"></span>
|
||||
</h3>
|
||||
<button id="copy-dns-btn" class="copy-btn">copy</button>
|
||||
</div>
|
||||
<div id="dns-lookup-loader" class="loader hidden mb-4"></div>
|
||||
<pre id="dns-lookup-output" class="result-pre"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="global-error" class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg"></div>
|
||||
</div>`,
|
||||
|
||||
async init(search) {
|
||||
const input = document.getElementById('dns-domain-input');
|
||||
const select = document.getElementById('dns-type-select');
|
||||
const btn = document.getElementById('dns-lookup-button');
|
||||
const errorEl = document.getElementById('dns-lookup-error');
|
||||
const section = document.getElementById('dns-lookup-results-section');
|
||||
const queryEl = document.getElementById('dns-lookup-query');
|
||||
const loader = document.getElementById('dns-lookup-loader');
|
||||
const output = document.getElementById('dns-lookup-output');
|
||||
const copyBtn = document.getElementById('copy-dns-btn');
|
||||
|
||||
const syncBtn = () => { btn.disabled = !input.value.trim(); };
|
||||
input.addEventListener('input', syncBtn);
|
||||
|
||||
setupCopyBtn(copyBtn, () => output.textContent);
|
||||
|
||||
async function doLookup() {
|
||||
const domain = input.value.trim();
|
||||
const type = select.value;
|
||||
if (!domain) return;
|
||||
|
||||
const url = new URL(location.href);
|
||||
url.searchParams.set('domain', domain);
|
||||
url.searchParams.set('type', type);
|
||||
history.replaceState({}, '', url);
|
||||
|
||||
showError(errorEl, null);
|
||||
section.classList.remove('hidden');
|
||||
loader.classList.remove('hidden');
|
||||
output.textContent = '';
|
||||
queryEl.textContent = `${domain} (${type})`;
|
||||
|
||||
try {
|
||||
const r = await fetch(`${API}/dns-lookup?domain=${encodeURIComponent(domain)}&type=${encodeURIComponent(type)}`);
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.success) throw new Error(data.error || `HTTP ${r.status}`);
|
||||
output.textContent = JSON.stringify(data.records, null, 2);
|
||||
} catch (err) {
|
||||
showError(errorEl, err.message);
|
||||
output.textContent = '';
|
||||
} finally {
|
||||
loader.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', doLookup);
|
||||
input.addEventListener('keypress', e => { if (e.key === 'Enter' && !btn.disabled) doLookup(); });
|
||||
|
||||
const params = new URLSearchParams(search);
|
||||
const d = params.get('domain');
|
||||
if (d) {
|
||||
input.value = d;
|
||||
const t = params.get('type');
|
||||
if (t) select.value = t;
|
||||
syncBtn();
|
||||
doLookup();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,671 @@
|
||||
import { API, setupCopyBtn } from '../shared.js';
|
||||
|
||||
export const page = {
|
||||
title: 'IP Info & Tools',
|
||||
|
||||
template: () => `
|
||||
<div class="container mx-auto max-w-5xl 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-8 text-center text-gradient glitch-text">Your Digital Footprint</h1>
|
||||
|
||||
<!-- Own IP info -->
|
||||
<div id="info-section" class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||
<!-- Left column -->
|
||||
<div class="space-y-6 fade-in" style="animation-delay:.1s">
|
||||
<div class="glass-card rounded-lg p-5 relative overflow-hidden group">
|
||||
<div class="absolute top-0 right-0 p-2 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-xs font-bold text-purple-400 uppercase tracking-widest mb-2">Your Public IP</h2>
|
||||
<div id="ip-info" class="min-h-[40px] flex items-center gap-2">
|
||||
<div id="ip-loader" class="loader"></div>
|
||||
<a id="ip-address-link" href="#" class="text-3xl font-mono font-bold text-white tracking-tight break-all hidden hover:text-purple-300 transition-colors" title="Click for WHOIS Lookup">
|
||||
<span id="ip-address"></span>
|
||||
</a>
|
||||
<button id="copy-ip-btn" class="copy-btn hidden">copy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card rounded-lg p-5 space-y-4">
|
||||
<div>
|
||||
<h2 class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">Location Details</h2>
|
||||
<div id="geo-info" class="min-h-[80px] space-y-1 text-sm text-gray-300">
|
||||
<div id="geo-loader" class="loader"></div>
|
||||
<div class="hidden grid grid-cols-2 gap-x-2 gap-y-1">
|
||||
<p><span class="text-gray-500">Country:</span> <span id="country" class="text-gray-200 font-medium">-</span></p>
|
||||
<p><span class="text-gray-500">Region:</span> <span id="region" class="text-gray-200 font-medium">-</span></p>
|
||||
<p><span class="text-gray-500">City:</span> <span id="city" class="text-gray-200 font-medium">-</span></p>
|
||||
<p><span class="text-gray-500">Zip:</span> <span id="postal" class="text-gray-200 font-medium">-</span></p>
|
||||
<p class="col-span-2"><span class="text-gray-500">Coords:</span> <span id="coords" class="font-mono text-xs text-purple-300">-</span></p>
|
||||
<p class="col-span-2"><span class="text-gray-500">Time:</span> <span id="timezone" class="text-gray-200 font-medium">-</span></p>
|
||||
<p id="geo-error" class="text-red-400 col-span-2 text-xs"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">Network (ASN)</h2>
|
||||
<div id="asn-info" class="min-h-[40px] space-y-1 text-sm text-gray-300">
|
||||
<div id="asn-loader" class="loader"></div>
|
||||
<div class="hidden">
|
||||
<p><span class="text-gray-500">AS Number:</span> <span id="asn-number" class="font-mono text-purple-300">-</span></p>
|
||||
<p><span class="text-gray-500">Org:</span> <span id="asn-org" class="font-medium text-white">-</span></p>
|
||||
<p id="asn-error" class="text-red-400 text-xs"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card rounded-lg p-5">
|
||||
<h2 class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">Hostname (rDNS)</h2>
|
||||
<div id="rdns-info" class="min-h-[30px] text-sm text-gray-300">
|
||||
<div id="rdns-loader" class="loader"></div>
|
||||
<div class="hidden">
|
||||
<ul id="rdns-list" class="list-none space-y-1 font-mono text-xs text-green-400"><li>-</li></ul>
|
||||
<p id="rdns-error" class="text-red-400 text-xs"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right column: map -->
|
||||
<div class="space-y-4 fade-in" style="animation-delay:.2s">
|
||||
<h2 class="text-lg font-semibold text-gray-200 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-purple-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Location Visualization
|
||||
</h2>
|
||||
<div id="map-container" class="bg-gray-800/50 rounded-lg min-h-[400px] h-full flex items-center justify-center relative border border-gray-700/50 shadow-inner overflow-hidden">
|
||||
<div id="map-loader" class="loader absolute z-10"></div>
|
||||
<div id="map" class="w-full h-full rounded-lg hidden z-0 opacity-80 hover:opacity-100 transition-opacity duration-700"></div>
|
||||
<p id="map-message" class="text-gray-400 hidden absolute text-sm">Could not load map.</p>
|
||||
<div class="absolute inset-0 pointer-events-none rounded-lg ring-1 ring-inset ring-white/10"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Lookup -->
|
||||
<div class="mt-8 p-6 glass-card rounded-xl">
|
||||
<h2 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-pink-500 mb-4 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-pink-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
IP Address / Domain Lookup
|
||||
</h2>
|
||||
<div class="flex flex-col sm:flex-row gap-3 mb-6">
|
||||
<input type="text" id="lookup-ip-input" placeholder="Enter IP or Domain (e.g., 8.8.8.8 or google.com)"
|
||||
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 hover:shadow-purple-500/25 transition-all duration-200">
|
||||
Lookup
|
||||
</button>
|
||||
</div>
|
||||
<div id="lookup-error" class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded text-sm"></div>
|
||||
|
||||
<div id="lookup-results-section" class="hidden grid grid-cols-1 md:grid-cols-2 gap-8 mt-6 border-t border-gray-700/50 pt-6 fade-in">
|
||||
<div class="space-y-6">
|
||||
<h3 class="text-lg font-semibold text-gray-200">Result for: <span id="lookup-ip-address" class="font-mono text-purple-400 bg-purple-500/10 px-2 py-0.5 rounded"></span>
|
||||
<button id="copy-lookup-ip-btn" class="copy-btn ml-2">copy</button>
|
||||
</h3>
|
||||
<div id="lookup-result-loader" class="loader hidden"></div>
|
||||
<div id="lookup-geo-info" class="space-y-1 text-sm text-gray-300">
|
||||
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Geolocation</h4>
|
||||
<div class="grid grid-cols-2 gap-x-2 gap-y-1">
|
||||
<p><span class="text-gray-500">Country:</span> <span id="lookup-country" class="text-white">-</span></p>
|
||||
<p><span class="text-gray-500">Region:</span> <span id="lookup-region" class="text-white">-</span></p>
|
||||
<p><span class="text-gray-500">City:</span> <span id="lookup-city" class="text-white">-</span></p>
|
||||
<p><span class="text-gray-500">Zip:</span> <span id="lookup-postal" class="text-white">-</span></p>
|
||||
<p class="col-span-2"><span class="text-gray-500">Coords:</span> <span id="lookup-coords" class="font-mono text-purple-300">-</span></p>
|
||||
<p class="col-span-2"><span class="text-gray-500">Time:</span> <span id="lookup-timezone" class="text-white">-</span></p>
|
||||
<p id="lookup-geo-error" class="text-red-400 col-span-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lookup-asn-info" class="space-y-1 text-sm text-gray-300">
|
||||
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2 mt-4">ASN</h4>
|
||||
<p><span class="text-gray-500">Number:</span> <span id="lookup-asn-number" class="text-white font-mono">-</span></p>
|
||||
<p><span class="text-gray-500">Org:</span> <span id="lookup-asn-org" class="text-white">-</span></p>
|
||||
<p id="lookup-asn-error" class="text-red-400"></p>
|
||||
</div>
|
||||
<div id="lookup-rdns-info" class="space-y-1 text-sm text-gray-300">
|
||||
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2 mt-4">Reverse DNS</h4>
|
||||
<ul id="lookup-rdns-list" class="list-none space-y-1 font-mono text-xs text-green-400"><li>-</li></ul>
|
||||
<p id="lookup-rdns-error" class="text-red-400"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Location Map</h4>
|
||||
<div id="lookup-map-container" class="glass-panel rounded-lg min-h-[250px] flex items-center justify-center relative overflow-hidden">
|
||||
<div id="lookup-map-loader" class="loader hidden absolute z-10"></div>
|
||||
<div id="lookup-map" class="w-full rounded hidden opacity-90"></div>
|
||||
<p id="lookup-map-message" class="text-gray-400 hidden absolute">Could not load map.</p>
|
||||
<div class="absolute inset-0 pointer-events-none ring-1 ring-inset ring-white/10 rounded-lg"></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 pt-2">
|
||||
<button id="lookup-ping-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Ping</button>
|
||||
<button id="lookup-trace-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Trace</button>
|
||||
<button id="lookup-scan-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Port Scan</button>
|
||||
</div>
|
||||
<div id="lookup-ping-results" class="mt-4 text-sm hidden fade-in">
|
||||
<h4 class="font-bold text-purple-400 mb-2 flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> Ping Results
|
||||
</h4>
|
||||
<div id="lookup-ping-loader" class="loader hidden"></div>
|
||||
<pre id="lookup-ping-output" class="result-pre mt-1"></pre>
|
||||
<p id="lookup-ping-error" class="text-red-400 mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Traceroute -->
|
||||
<div id="traceroute-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in">
|
||||
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4">Traceroute Results</h2>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<div id="traceroute-loader" class="loader hidden"></div>
|
||||
<span id="traceroute-message" class="text-gray-300"></span>
|
||||
</div>
|
||||
<button id="traceroute-stop-btn" class="stop-btn hidden">■ Stop</button>
|
||||
</div>
|
||||
<div id="traceroute-output" class="rounded-lg overflow-hidden"><pre class="m-0"></pre></div>
|
||||
</div>
|
||||
|
||||
<!-- Port Scan -->
|
||||
<div id="port-scan-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in">
|
||||
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4">Port Scan Results</h2>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<div id="port-scan-loader" class="loader hidden"></div>
|
||||
<span id="port-scan-message" class="text-gray-300"></span>
|
||||
</div>
|
||||
<button id="port-scan-stop-btn" class="stop-btn hidden">■ Stop</button>
|
||||
</div>
|
||||
<div id="port-scan-output" class="text-sm font-mono bg-gray-900/50 p-4 rounded-lg border border-gray-700/50 max-h-[300px] overflow-y-auto"></div>
|
||||
</div>
|
||||
|
||||
<div id="global-error" class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg"></div>
|
||||
</div>`,
|
||||
|
||||
async init(search) {
|
||||
// ── DOM refs ──────────────────────────────────────────────────
|
||||
const ipAddressLink = document.getElementById('ip-address-link');
|
||||
const ipAddressSpan = document.getElementById('ip-address');
|
||||
const copyIpBtn = document.getElementById('copy-ip-btn');
|
||||
const ipLoader = document.getElementById('ip-loader');
|
||||
const geoLoader = document.getElementById('geo-loader');
|
||||
const asnLoader = document.getElementById('asn-loader');
|
||||
const rdnsLoader = document.getElementById('rdns-loader');
|
||||
const mapLoader = document.getElementById('map-loader');
|
||||
const mapEl = document.getElementById('map');
|
||||
const mapMessage = document.getElementById('map-message');
|
||||
const globalError = document.getElementById('global-error');
|
||||
|
||||
const lookupInput = document.getElementById('lookup-ip-input');
|
||||
const lookupBtn = document.getElementById('lookup-button');
|
||||
const lookupErrorEl = document.getElementById('lookup-error');
|
||||
const lookupSection = document.getElementById('lookup-results-section');
|
||||
const lookupIpEl = document.getElementById('lookup-ip-address');
|
||||
const copyLookupIpBtn = document.getElementById('copy-lookup-ip-btn');
|
||||
const lookupResLoader = document.getElementById('lookup-result-loader');
|
||||
const lookupMapEl = document.getElementById('lookup-map');
|
||||
const lookupMapLoader = document.getElementById('lookup-map-loader');
|
||||
const lookupMapMsg = document.getElementById('lookup-map-message');
|
||||
const lookupPingBtn = document.getElementById('lookup-ping-button');
|
||||
const lookupTraceBtn = document.getElementById('lookup-trace-button');
|
||||
const lookupScanBtn = document.getElementById('lookup-scan-button');
|
||||
const lookupPingRes = document.getElementById('lookup-ping-results');
|
||||
const lookupPingLoader= document.getElementById('lookup-ping-loader');
|
||||
const lookupPingOutput= document.getElementById('lookup-ping-output');
|
||||
const lookupPingError = document.getElementById('lookup-ping-error');
|
||||
|
||||
const tracerouteSection = document.getElementById('traceroute-section');
|
||||
const tracerouteOutput = document.querySelector('#traceroute-output pre');
|
||||
const tracerouteLoader = document.getElementById('traceroute-loader');
|
||||
const tracerouteMessage = document.getElementById('traceroute-message');
|
||||
const tracerouteStopBtn = document.getElementById('traceroute-stop-btn');
|
||||
|
||||
const portScanSection = document.getElementById('port-scan-section');
|
||||
const portScanOutput = document.getElementById('port-scan-output');
|
||||
const portScanLoader = document.getElementById('port-scan-loader');
|
||||
const portScanMessage = document.getElementById('port-scan-message');
|
||||
const portScanStopBtn = document.getElementById('port-scan-stop-btn');
|
||||
|
||||
// ── State ────────────────────────────────────────────────────
|
||||
let map = null, lookupMap = null, currentIp = null, currentLookupIp = null;
|
||||
let eventSource = null, portScanEventSource = null;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────
|
||||
function showGlobalErr(msg) {
|
||||
if (!globalError) return;
|
||||
globalError.textContent = `Error: ${msg}`;
|
||||
globalError.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function isValidIp(input) {
|
||||
const v4 = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
|
||||
const v6 = /^(?:(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,7}:|(?:[a-fA-F0-9]{1,4}:){1,6}:[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,5}(?::[a-fA-F0-9]{1,4}){1,2}|(?:[a-fA-F0-9]{1,4}:){1,4}(?::[a-fA-F0-9]{1,4}){1,3}|(?:[a-fA-F0-9]{1,4}:){1,3}(?::[a-fA-F0-9]{1,4}){1,4}|(?:[a-fA-F0-9]{1,4}:){1,2}(?::[a-fA-F0-9]{1,4}){1,5}|[a-fA-F0-9]{1,4}:(?::[a-fA-F0-9]{1,4}){1,6}|:(?::[a-fA-F0-9]{1,4}){1,7}|::)$/;
|
||||
return v4.test(input) || v6.test(input);
|
||||
}
|
||||
|
||||
function updateField(el, val, loaderEl = null, errorEl = null, def = '-') {
|
||||
if (loaderEl) loaderEl.classList.add('hidden');
|
||||
if (errorEl) errorEl.textContent = '';
|
||||
const container = el?.closest('div:not(.loader)');
|
||||
if (container?.classList.contains('hidden')) container.classList.remove('hidden');
|
||||
if (val && typeof val === 'object' && val.error) {
|
||||
if (el) el.textContent = def;
|
||||
if (errorEl) errorEl.textContent = val.error;
|
||||
} else if (val != null && val !== '') {
|
||||
if (el) el.textContent = val;
|
||||
} else {
|
||||
if (el) el.textContent = def;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRdns(listEl, data, loaderEl = null, errorEl = null) {
|
||||
if (loaderEl) loaderEl.classList.add('hidden');
|
||||
if (listEl) listEl.innerHTML = '';
|
||||
if (errorEl) errorEl.textContent = '';
|
||||
const container = listEl?.closest('div:not(.loader)');
|
||||
if (container?.classList.contains('hidden')) container.classList.remove('hidden');
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length > 0) data.forEach(h => { const li = document.createElement('li'); li.textContent = h; listEl.appendChild(li); });
|
||||
else if (listEl) listEl.innerHTML = '<li>No rDNS records found.</li>';
|
||||
} else if (data?.error) {
|
||||
if (listEl) listEl.innerHTML = '<li>-</li>';
|
||||
if (errorEl) errorEl.textContent = data.error;
|
||||
} else {
|
||||
if (listEl) listEl.innerHTML = '<li>-</li>';
|
||||
}
|
||||
}
|
||||
|
||||
function initOrUpdateMap(mapId, lat, lon, mapElement, loaderEl, msgEl) {
|
||||
if (!mapElement || !loaderEl || !msgEl) return null;
|
||||
loaderEl.classList.add('hidden');
|
||||
let inst = window[mapId + '_instance'];
|
||||
if (lat != null && lon != null) {
|
||||
mapElement.classList.remove('hidden');
|
||||
msgEl.classList.add('hidden');
|
||||
if (inst) {
|
||||
inst.setView([lat, lon], 13);
|
||||
inst.eachLayer(l => { if (l instanceof L.Marker) inst.removeLayer(l); });
|
||||
L.marker([lat, lon]).addTo(inst).bindPopup('Approximate Location').openPopup();
|
||||
} else {
|
||||
try {
|
||||
inst = L.map(mapId).setView([lat, lon], 13);
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
subdomains: 'abcd', maxZoom: 19
|
||||
}).addTo(inst);
|
||||
L.marker([lat, lon]).addTo(inst).bindPopup('Approximate Location').openPopup();
|
||||
window[mapId + '_instance'] = inst;
|
||||
} catch (e) {
|
||||
console.error('Map init failed:', e);
|
||||
mapElement.classList.add('hidden');
|
||||
msgEl.classList.remove('hidden');
|
||||
msgEl.textContent = 'Error initializing map.';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
setTimeout(() => { if (window[mapId + '_instance']) window[mapId + '_instance'].invalidateSize(); }, 100);
|
||||
return inst;
|
||||
} else {
|
||||
mapElement.classList.add('hidden');
|
||||
msgEl.classList.remove('hidden');
|
||||
msgEl.textContent = 'Map could not be loaded (missing coordinates).';
|
||||
if (inst) { inst.remove(); window[mapId + '_instance'] = null; }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Copy buttons ─────────────────────────────────────────────
|
||||
setupCopyBtn(copyIpBtn, () => ipAddressSpan.textContent);
|
||||
setupCopyBtn(copyLookupIpBtn, () => lookupIpEl.textContent);
|
||||
|
||||
// ── Lookup button enable/disable ─────────────────────────────
|
||||
const syncLookupBtn = () => { lookupBtn.disabled = !lookupInput.value.trim(); };
|
||||
lookupInput.addEventListener('input', syncLookupBtn);
|
||||
|
||||
// ── Own IP fetch ─────────────────────────────────────────────
|
||||
async function fetchIpInfo() {
|
||||
globalError.classList.add('hidden');
|
||||
[ipLoader, geoLoader, asnLoader, rdnsLoader, mapLoader].forEach(l => l?.classList.remove('hidden'));
|
||||
ipAddressLink.classList.add('hidden');
|
||||
copyIpBtn.classList.add('hidden');
|
||||
mapEl.classList.add('hidden');
|
||||
mapMessage.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const r = await fetch(`${API}/ipinfo`);
|
||||
if (!r.ok) throw new Error(`${r.statusText} (${r.status})`);
|
||||
const data = await r.json();
|
||||
currentIp = data.ip;
|
||||
|
||||
ipAddressSpan.textContent = data.ip;
|
||||
ipAddressLink.classList.remove('hidden');
|
||||
copyIpBtn.classList.remove('hidden');
|
||||
ipLoader.classList.add('hidden');
|
||||
ipAddressLink.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
if (currentIp) window._router.navigate('/whois', { query: currentIp });
|
||||
});
|
||||
|
||||
updateField(document.getElementById('country'), data.geo?.countryName ? `${data.geo.countryName} (${data.geo.country})` : null, null, document.getElementById('geo-error'));
|
||||
updateField(document.getElementById('region'), data.geo?.region);
|
||||
updateField(document.getElementById('city'), data.geo?.city);
|
||||
updateField(document.getElementById('postal'), data.geo?.postalCode);
|
||||
updateField(document.getElementById('coords'), data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null);
|
||||
updateField(document.getElementById('timezone'), data.geo?.timezone, geoLoader);
|
||||
|
||||
const asnNum = (data.asn && !data.asn.error) ? data.asn.number : null;
|
||||
if (asnNum) {
|
||||
const asnContainer = document.getElementById('asn-number')?.closest('div:not(.loader)');
|
||||
if (asnContainer) asnContainer.classList.remove('hidden');
|
||||
document.getElementById('asn-number').innerHTML =
|
||||
`<a href="/asn?asn=${asnNum}" class="hover:text-purple-200 underline decoration-dotted transition-colors" title="Open ASN Lookup">AS${asnNum}</a>`;
|
||||
} else {
|
||||
updateField(document.getElementById('asn-number'), null, null, document.getElementById('asn-error'), data.asn?.error || '-');
|
||||
}
|
||||
updateField(document.getElementById('asn-org'), data.asn?.organization, asnLoader);
|
||||
updateRdns(document.getElementById('rdns-list'), data.rdns, rdnsLoader, document.getElementById('rdns-error'));
|
||||
map = initOrUpdateMap('map', data.geo?.latitude, data.geo?.longitude, mapEl, mapLoader, mapMessage);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch IP info:', err);
|
||||
showGlobalErr(`Could not load IP information. ${err.message}`);
|
||||
[ipLoader, geoLoader, asnLoader, rdnsLoader, mapLoader].forEach(l => l?.classList.add('hidden'));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lookup ───────────────────────────────────────────────────
|
||||
function resetLookup() {
|
||||
lookupSection.classList.add('hidden');
|
||||
lookupResLoader.classList.add('hidden');
|
||||
lookupMapLoader.classList.add('hidden');
|
||||
lookupMapEl.classList.add('hidden');
|
||||
lookupMapMsg.classList.add('hidden');
|
||||
lookupPingRes.classList.add('hidden');
|
||||
lookupPingLoader.classList.add('hidden');
|
||||
portScanSection.classList.add('hidden');
|
||||
portScanOutput.innerHTML = '';
|
||||
[lookupIpEl, document.getElementById('lookup-country'), document.getElementById('lookup-region'),
|
||||
document.getElementById('lookup-city'), document.getElementById('lookup-postal'),
|
||||
document.getElementById('lookup-coords'), document.getElementById('lookup-timezone'),
|
||||
document.getElementById('lookup-asn-number'), document.getElementById('lookup-asn-org'),
|
||||
document.getElementById('lookup-geo-error'), document.getElementById('lookup-asn-error'),
|
||||
document.getElementById('lookup-rdns-error')].forEach(el => { if (el) el.textContent = ''; });
|
||||
document.getElementById('lookup-rdns-list').innerHTML = '<li>-</li>';
|
||||
lookupPingOutput.textContent = '';
|
||||
lookupPingError.textContent = '';
|
||||
[lookupPingBtn, lookupTraceBtn, lookupScanBtn].forEach(b => { if (b) b.disabled = true; });
|
||||
currentLookupIp = null;
|
||||
if (window['lookup-map_instance']) { window['lookup-map_instance'].remove(); window['lookup-map_instance'] = null; }
|
||||
if (portScanEventSource) { portScanEventSource.close(); portScanEventSource = null; }
|
||||
}
|
||||
|
||||
async function doLookup(query) {
|
||||
resetLookup();
|
||||
lookupErrorEl.classList.add('hidden');
|
||||
globalError.classList.add('hidden');
|
||||
|
||||
const url = new URL(location.href);
|
||||
url.searchParams.set('ip', query);
|
||||
history.replaceState({}, '', url);
|
||||
|
||||
lookupSection.classList.remove('hidden');
|
||||
lookupResLoader.classList.remove('hidden');
|
||||
lookupMapLoader.classList.remove('hidden');
|
||||
|
||||
let ipToLookup = query;
|
||||
if (!isValidIp(query)) {
|
||||
try {
|
||||
const r = await fetch(`${API}/dns-lookup?domain=${encodeURIComponent(query)}&type=A`);
|
||||
const data = await r.json();
|
||||
if (r.ok && data.success && data.records?.length) {
|
||||
ipToLookup = Array.isArray(data.records) ? data.records[0] : data.records;
|
||||
} else {
|
||||
const r2 = await fetch(`${API}/dns-lookup?domain=${encodeURIComponent(query)}&type=AAAA`);
|
||||
const data2 = await r2.json();
|
||||
if (r2.ok && data2.success && data2.records?.length) {
|
||||
ipToLookup = Array.isArray(data2.records) ? data2.records[0] : data2.records;
|
||||
} else {
|
||||
throw new Error(data.error || 'No A or AAAA records found.');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
lookupErrorEl.textContent = `Error: Could not resolve domain — ${err.message}`;
|
||||
lookupErrorEl.classList.remove('hidden');
|
||||
resetLookup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await fetch(`${API}/lookup?targetIp=${encodeURIComponent(ipToLookup)}`);
|
||||
const data = await r.json();
|
||||
if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`);
|
||||
currentLookupIp = data.ip;
|
||||
|
||||
updateField(lookupIpEl, data.ip);
|
||||
updateField(document.getElementById('lookup-country'), data.geo?.countryName ? `${data.geo.countryName} (${data.geo.country})` : null, null, document.getElementById('lookup-geo-error'));
|
||||
updateField(document.getElementById('lookup-region'), data.geo?.region);
|
||||
updateField(document.getElementById('lookup-city'), data.geo?.city);
|
||||
updateField(document.getElementById('lookup-postal'), data.geo?.postalCode);
|
||||
updateField(document.getElementById('lookup-coords'), data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null);
|
||||
updateField(document.getElementById('lookup-timezone'), data.geo?.timezone);
|
||||
|
||||
if (data.asn?.number) {
|
||||
document.getElementById('lookup-asn-number').innerHTML =
|
||||
`<a href="/asn?asn=${data.asn.number}" class="text-purple-400 hover:text-purple-300 underline decoration-dotted transition-colors font-mono">AS${data.asn.number}</a>`;
|
||||
} else {
|
||||
updateField(document.getElementById('lookup-asn-number'), data.asn?.number, null, document.getElementById('lookup-asn-error'));
|
||||
}
|
||||
updateField(document.getElementById('lookup-asn-org'), data.asn?.organization);
|
||||
updateRdns(document.getElementById('lookup-rdns-list'), data.rdns, null, document.getElementById('lookup-rdns-error'));
|
||||
lookupMap = initOrUpdateMap('lookup-map', data.geo?.latitude, data.geo?.longitude, lookupMapEl, lookupMapLoader, lookupMapMsg);
|
||||
[lookupPingBtn, lookupTraceBtn, lookupScanBtn].forEach(b => { if (b) b.disabled = false; });
|
||||
|
||||
} catch (err) {
|
||||
lookupErrorEl.textContent = `Error: Lookup failed — ${err.message}`;
|
||||
lookupErrorEl.classList.remove('hidden');
|
||||
lookupMapMsg.textContent = 'Map could not be loaded.';
|
||||
lookupMapMsg.classList.remove('hidden');
|
||||
lookupMapEl.classList.add('hidden');
|
||||
lookupMapLoader.classList.add('hidden');
|
||||
resetLookup();
|
||||
} finally {
|
||||
lookupResLoader.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ping ─────────────────────────────────────────────────────
|
||||
async function runPing(ip) {
|
||||
lookupPingRes.classList.remove('hidden');
|
||||
lookupPingLoader.classList.remove('hidden');
|
||||
lookupPingOutput.textContent = '';
|
||||
lookupPingError.textContent = '';
|
||||
try {
|
||||
const r = await fetch(`${API}/ping?targetIp=${encodeURIComponent(ip)}`);
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.success) throw new Error(data.error || `HTTP ${r.status}`);
|
||||
let out = `--- Ping Statistics for ${ip} ---\n`;
|
||||
if (data.stats) {
|
||||
out += `Packets: ${data.stats.packets.transmitted} sent, ${data.stats.packets.received} received, ${data.stats.packets.lossPercent}% loss\n`;
|
||||
if (data.stats.rtt) out += `RTT (ms): min=${data.stats.rtt.min} avg=${data.stats.rtt.avg} max=${data.stats.rtt.max}\n`;
|
||||
}
|
||||
out += `\n--- Raw Output ---\n${data.rawOutput || ''}`;
|
||||
lookupPingOutput.textContent = out;
|
||||
} catch (err) {
|
||||
lookupPingError.textContent = `Ping Error: ${err.message}`;
|
||||
} finally {
|
||||
lookupPingLoader.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Traceroute ───────────────────────────────────────────────
|
||||
function startTraceroute(ip) {
|
||||
if (eventSource) { eventSource.close(); eventSource = null; }
|
||||
tracerouteSection.classList.remove('hidden');
|
||||
tracerouteOutput.textContent = '';
|
||||
tracerouteLoader.classList.remove('hidden');
|
||||
tracerouteStopBtn.classList.remove('hidden');
|
||||
tracerouteMessage.textContent = `Starting traceroute to ${ip}…`;
|
||||
globalError.classList.add('hidden');
|
||||
|
||||
eventSource = new EventSource(`${API}/traceroute?targetIp=${encodeURIComponent(ip)}`);
|
||||
|
||||
eventSource.onopen = () => { tracerouteMessage.textContent = `Traceroute to ${ip} in progress…`; };
|
||||
eventSource.onerror = () => {
|
||||
tracerouteMessage.textContent = eventSource.readyState === EventSource.CLOSED
|
||||
? 'Connection closed.' : 'Connection error.';
|
||||
tracerouteLoader.classList.add('hidden');
|
||||
tracerouteStopBtn.classList.add('hidden');
|
||||
eventSource.close();
|
||||
};
|
||||
eventSource.addEventListener('hop', e => {
|
||||
try { displayHop(JSON.parse(e.data)); } catch { displayTraceLine(`[Parse error: ${e.data}]`, 'error-line'); }
|
||||
});
|
||||
eventSource.addEventListener('info', e => {
|
||||
try { displayTraceLine(JSON.parse(e.data).message, 'info-line'); } catch {}
|
||||
});
|
||||
eventSource.addEventListener('error', e => {
|
||||
try { const d = JSON.parse(e.data); displayTraceLine(d.error, 'error-line'); } catch {}
|
||||
});
|
||||
eventSource.addEventListener('end', e => {
|
||||
try {
|
||||
const d = JSON.parse(e.data);
|
||||
const msg = `Traceroute finished${d.exitCode === 0 ? ' successfully' : ` (exit code ${d.exitCode})`}.`;
|
||||
displayTraceLine(msg, 'end-line');
|
||||
tracerouteMessage.textContent = msg;
|
||||
} catch {}
|
||||
tracerouteLoader.classList.add('hidden');
|
||||
tracerouteStopBtn.classList.add('hidden');
|
||||
eventSource.close();
|
||||
});
|
||||
}
|
||||
|
||||
function stopTraceroute() {
|
||||
if (eventSource) { eventSource.close(); eventSource = null; }
|
||||
tracerouteLoader.classList.add('hidden');
|
||||
tracerouteStopBtn.classList.add('hidden');
|
||||
tracerouteMessage.textContent = 'Traceroute stopped.';
|
||||
displayTraceLine('— Stopped by user —', 'info-line');
|
||||
}
|
||||
|
||||
function displayTraceLine(text, cls = '') {
|
||||
const div = document.createElement('div');
|
||||
if (cls) div.classList.add(cls);
|
||||
div.classList.add('fade-in');
|
||||
div.textContent = text;
|
||||
tracerouteOutput.appendChild(div);
|
||||
tracerouteOutput.scrollTop = tracerouteOutput.scrollHeight;
|
||||
}
|
||||
|
||||
function displayHop(hop) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('hop-line', 'fade-in');
|
||||
const num = document.createElement('span'); num.classList.add('hop-number'); num.textContent = hop.hop || '?'; div.appendChild(num);
|
||||
if (hop.ip) {
|
||||
const ip = document.createElement('span'); ip.classList.add('hop-ip'); ip.textContent = hop.ip; div.appendChild(ip);
|
||||
if (hop.hostname) { const h = document.createElement('span'); h.classList.add('hop-hostname'); h.textContent = ` (${hop.hostname})`; div.appendChild(h); }
|
||||
} else if (hop.rtt?.every(r => r === '*')) {
|
||||
const t = document.createElement('span'); t.classList.add('hop-timeout'); t.textContent = '* * *'; div.appendChild(t);
|
||||
} else {
|
||||
div.appendChild(document.createTextNode(hop.rawLine || 'Unknown hop'));
|
||||
}
|
||||
if (Array.isArray(hop.rtt)) {
|
||||
hop.rtt.forEach(r => {
|
||||
const s = document.createElement('span');
|
||||
s.classList.add(r === '*' ? 'hop-timeout' : 'hop-rtt');
|
||||
s.textContent = r === '*' ? ' *' : ` ${r} ms`;
|
||||
div.appendChild(s);
|
||||
});
|
||||
}
|
||||
tracerouteOutput.appendChild(div);
|
||||
tracerouteOutput.scrollTop = tracerouteOutput.scrollHeight;
|
||||
}
|
||||
|
||||
// ── Port Scan ────────────────────────────────────────────────
|
||||
function startPortScan(ip) {
|
||||
if (portScanEventSource) { portScanEventSource.close(); portScanEventSource = null; }
|
||||
portScanSection.classList.remove('hidden');
|
||||
portScanOutput.innerHTML = '';
|
||||
portScanLoader.classList.remove('hidden');
|
||||
portScanStopBtn.classList.remove('hidden');
|
||||
portScanMessage.textContent = `Starting port scan for ${ip}…`;
|
||||
|
||||
portScanEventSource = new EventSource(`${API}/port-scan?targetIp=${encodeURIComponent(ip)}`);
|
||||
portScanEventSource.onopen = () => {};
|
||||
portScanEventSource.onerror = () => {
|
||||
portScanMessage.textContent = 'Connection error during port scan.';
|
||||
portScanLoader.classList.add('hidden');
|
||||
portScanStopBtn.classList.add('hidden');
|
||||
portScanEventSource.close();
|
||||
};
|
||||
portScanEventSource.addEventListener('info', e => {
|
||||
try { portScanMessage.textContent = JSON.parse(e.data).message; } catch {}
|
||||
});
|
||||
portScanEventSource.addEventListener('port_status', e => {
|
||||
try { displayPortResult(JSON.parse(e.data)); } catch {}
|
||||
});
|
||||
portScanEventSource.addEventListener('error', e => {
|
||||
try { displayPortResult({ error: JSON.parse(e.data).error }); } catch {}
|
||||
});
|
||||
portScanEventSource.addEventListener('end', e => {
|
||||
try { portScanMessage.textContent = JSON.parse(e.data).message; } catch {}
|
||||
portScanLoader.classList.add('hidden');
|
||||
portScanStopBtn.classList.add('hidden');
|
||||
portScanEventSource.close();
|
||||
});
|
||||
}
|
||||
|
||||
function stopPortScan() {
|
||||
if (portScanEventSource) { portScanEventSource.close(); portScanEventSource = null; }
|
||||
portScanLoader.classList.add('hidden');
|
||||
portScanStopBtn.classList.add('hidden');
|
||||
portScanMessage.textContent = 'Port scan stopped.';
|
||||
}
|
||||
|
||||
function displayPortResult(data) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('mb-1', 'fade-in');
|
||||
if (data.error) {
|
||||
div.innerHTML = `<span class="text-red-400">Error: ${data.error}</span>`;
|
||||
} else {
|
||||
const colors = { open: 'text-green-400', closed: 'text-red-400', timeout: 'text-yellow-400' };
|
||||
const labels = { open: 'OPEN', closed: 'CLOSED', timeout: 'TIMEOUT (Filtered?)' };
|
||||
const col = colors[data.status] || 'text-gray-400';
|
||||
const lbl = labels[data.status] || (data.status || '').toUpperCase();
|
||||
div.innerHTML = `Port <span class="font-bold w-12 inline-block">${data.port}</span> <span class="w-24 inline-block">(${data.service})</span>: <span class="font-bold ${col}">${lbl}</span>`;
|
||||
}
|
||||
portScanOutput.appendChild(div);
|
||||
portScanOutput.scrollTop = portScanOutput.scrollHeight;
|
||||
}
|
||||
|
||||
// ── Event listeners ──────────────────────────────────────────
|
||||
lookupBtn.addEventListener('click', () => { const q = lookupInput.value.trim(); if (q) doLookup(q); });
|
||||
lookupInput.addEventListener('keypress', e => { if (e.key === 'Enter' && !lookupBtn.disabled) { const q = lookupInput.value.trim(); if (q) doLookup(q); } });
|
||||
lookupPingBtn.addEventListener('click', () => { if (currentLookupIp) runPing(currentLookupIp); });
|
||||
lookupTraceBtn.addEventListener('click', () => { if (currentLookupIp) startTraceroute(currentLookupIp); });
|
||||
lookupScanBtn.addEventListener('click', () => { if (currentLookupIp) startPortScan(currentLookupIp); });
|
||||
tracerouteStopBtn.addEventListener('click', stopTraceroute);
|
||||
portScanStopBtn.addEventListener('click', stopPortScan);
|
||||
|
||||
// ── Bootstrap ────────────────────────────────────────────────
|
||||
fetchIpInfo();
|
||||
|
||||
const params = new URLSearchParams(search);
|
||||
const ipParam = params.get('ip');
|
||||
if (ipParam) { lookupInput.value = ipParam; syncLookupBtn(); doLookup(ipParam); }
|
||||
|
||||
// ── Cleanup ──────────────────────────────────────────────────
|
||||
return () => {
|
||||
if (eventSource) { eventSource.close(); eventSource = null; }
|
||||
if (portScanEventSource) { portScanEventSource.close(); portScanEventSource = null; }
|
||||
if (window['map_instance']) { window['map_instance'].remove(); window['map_instance'] = null; }
|
||||
if (window['lookup-map_instance']) { window['lookup-map_instance'].remove(); window['lookup-map_instance'] = null; }
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { API, showError } from '../shared.js';
|
||||
|
||||
export const page = {
|
||||
title: 'MAC Vendor Lookup',
|
||||
|
||||
template: () => `
|
||||
<div class="container mx-auto max-w-5xl 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-8 text-center text-gradient">MAC Address Vendor Lookup</h1>
|
||||
|
||||
<div class="p-6 glass-card rounded-xl">
|
||||
<div class="flex flex-col sm:flex-row gap-3 mb-6">
|
||||
<input type="text" id="mac-input" placeholder="Enter MAC address (e.g., 00:1A:2B:3C:4D:5E)"
|
||||
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="mac-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">
|
||||
Find Vendor
|
||||
</button>
|
||||
</div>
|
||||
<div id="mac-lookup-error" class="hidden mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-400 text-sm"></div>
|
||||
|
||||
<div id="mac-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in">
|
||||
<h3 class="text-lg font-semibold text-purple-300 mb-4 flex items-center justify-center gap-2">
|
||||
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div>
|
||||
Vendor for: <span id="mac-lookup-query" class="font-mono text-purple-400 ml-1"></span>
|
||||
</h3>
|
||||
<div id="mac-lookup-loader" class="loader hidden mb-4 mx-auto"></div>
|
||||
<pre id="mac-lookup-output" class="result-pre text-center text-xl"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="global-error" class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg"></div>
|
||||
</div>`,
|
||||
|
||||
async init(search) {
|
||||
const input = document.getElementById('mac-input');
|
||||
const btn = document.getElementById('mac-lookup-button');
|
||||
const errorEl = document.getElementById('mac-lookup-error');
|
||||
const section = document.getElementById('mac-lookup-results-section');
|
||||
const queryEl = document.getElementById('mac-lookup-query');
|
||||
const loader = document.getElementById('mac-lookup-loader');
|
||||
const output = document.getElementById('mac-lookup-output');
|
||||
|
||||
const syncBtn = () => { btn.disabled = !input.value.trim(); };
|
||||
input.addEventListener('input', syncBtn);
|
||||
|
||||
async function doLookup() {
|
||||
const mac = input.value.trim();
|
||||
if (!mac) return;
|
||||
|
||||
showError(errorEl, null);
|
||||
section.classList.remove('hidden');
|
||||
loader.classList.remove('hidden');
|
||||
output.textContent = '';
|
||||
queryEl.textContent = mac;
|
||||
|
||||
try {
|
||||
const r = await fetch(`${API}/mac-lookup?mac=${encodeURIComponent(mac)}`);
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.success) throw new Error(data.error || `HTTP ${r.status}`);
|
||||
output.textContent = data.vendor || 'No vendor found.';
|
||||
} catch (err) {
|
||||
showError(errorEl, err.message);
|
||||
output.textContent = '';
|
||||
} finally {
|
||||
loader.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', doLookup);
|
||||
input.addEventListener('keypress', e => { if (e.key === 'Enter' && !btn.disabled) doLookup(); });
|
||||
|
||||
const params = new URLSearchParams(search);
|
||||
const m = params.get('mac');
|
||||
if (m) { input.value = m; syncBtn(); doLookup(); }
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,205 @@
|
||||
export const page = {
|
||||
title: 'Subnetz Rechner',
|
||||
|
||||
template: () => `
|
||||
<div class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
|
||||
<h2 class="text-3xl font-bold mb-8 text-center text-gradient">IP Subnetz Rechner</h2>
|
||||
|
||||
<form id="subnet-form" class="mb-8 glass-card p-6 rounded-xl">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
||||
<div>
|
||||
<label for="ip-address" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">IP Adresse:</label>
|
||||
<input type="text" id="ip-address" name="ip-address" placeholder="z.B. 192.168.1.1" required
|
||||
class="w-full 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">
|
||||
</div>
|
||||
<div>
|
||||
<label for="cidr" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">CIDR / Maske:</label>
|
||||
<input type="text" id="cidr" name="cidr" placeholder="z.B. 24 oder 255.255.255.0" required
|
||||
class="w-full 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">
|
||||
</div>
|
||||
</div>
|
||||
<div id="subnet-error" class="hidden mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-400 text-sm"></div>
|
||||
<button type="submit"
|
||||
class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200 ease-in-out transform hover:-translate-y-0.5">
|
||||
Berechnen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="results" class="glass-card rounded-xl p-6 hidden fade-in">
|
||||
<h3 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Ergebnisse:
|
||||
</h3>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
|
||||
<span class="text-gray-400">Netzwerkadresse:</span>
|
||||
<span id="network-address" class="font-mono text-white font-semibold">-</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
|
||||
<span class="text-gray-400">Broadcast-Adresse:</span>
|
||||
<span id="broadcast-address" class="font-mono text-purple-400 font-semibold">-</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
|
||||
<span class="text-gray-400">Subnetzmaske:</span>
|
||||
<span id="subnet-mask" class="font-mono text-gray-300">-</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
|
||||
<span class="text-gray-400">Anzahl der Hosts:</span>
|
||||
<span id="host-count" class="font-mono text-green-400 font-bold">-</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
|
||||
<span class="text-gray-400">Erste Host-Adresse:</span>
|
||||
<span id="first-host" class="font-mono text-blue-300">-</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<span class="text-gray-400">Letzte Host-Adresse:</span>
|
||||
<span id="last-host" class="font-mono text-blue-300">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example subnets -->
|
||||
<div class="glass-card rounded-xl p-6 mt-8">
|
||||
<h3 class="text-lg font-bold text-gray-400 uppercase tracking-wider border-b border-gray-700/50 pb-2 mb-4">
|
||||
Beispiel-Subnetze (Private Adressbereiche)
|
||||
</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm text-left text-gray-400">
|
||||
<thead class="text-xs uppercase bg-gray-800/50 text-gray-200">
|
||||
<tr>
|
||||
<th class="px-6 py-3">Bereich</th>
|
||||
<th class="px-6 py-3">CIDR</th>
|
||||
<th class="px-6 py-3">Subnetzmaske</th>
|
||||
<th class="px-6 py-3">Beschreibung</th>
|
||||
<th class="px-6 py-3">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-700/50">
|
||||
<tr class="hover:bg-gray-700/30 transition-colors">
|
||||
<td class="px-6 py-4 font-mono text-white">192.168.0.0 – 192.168.255.255</td>
|
||||
<td class="px-6 py-4 font-mono">/16 (Gesamt)</td>
|
||||
<td class="px-6 py-4 font-mono">255.255.0.0</td>
|
||||
<td class="px-6 py-4">Klasse C (oft als /24 genutzt)</td>
|
||||
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="192.168.1.1" data-cidr="24">Beispiel /24</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-700/30 transition-colors">
|
||||
<td class="px-6 py-4 font-mono text-white">172.16.0.0 – 172.31.255.255</td>
|
||||
<td class="px-6 py-4 font-mono">/12 (Gesamt)</td>
|
||||
<td class="px-6 py-4 font-mono">255.240.0.0</td>
|
||||
<td class="px-6 py-4">Klasse B</td>
|
||||
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="172.16.10.5" data-cidr="16">Beispiel /16</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-700/30 transition-colors">
|
||||
<td class="px-6 py-4 font-mono text-white">10.0.0.0 – 10.255.255.255</td>
|
||||
<td class="px-6 py-4 font-mono">/8 (Gesamt)</td>
|
||||
<td class="px-6 py-4 font-mono">255.0.0.0</td>
|
||||
<td class="px-6 py-4">Klasse A</td>
|
||||
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="10.0.50.100" data-cidr="8">Beispiel /8</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="mt-4 text-xs text-gray-500 italic">Klicken Sie auf "Beispiel", um die Felder auszufüllen und die Berechnung zu starten.</p>
|
||||
</div>
|
||||
</div>`,
|
||||
|
||||
init() {
|
||||
const form = document.getElementById('subnet-form');
|
||||
const ipInput = document.getElementById('ip-address');
|
||||
const cidrInput = document.getElementById('cidr');
|
||||
const errorEl = document.getElementById('subnet-error');
|
||||
const resultsEl = document.getElementById('results');
|
||||
|
||||
function showInlineError(msg) {
|
||||
errorEl.textContent = msg;
|
||||
errorEl.classList.toggle('hidden', !msg);
|
||||
}
|
||||
|
||||
function isValidIP(ip) {
|
||||
return /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(ip);
|
||||
}
|
||||
|
||||
function ipToBinary(ip) {
|
||||
return ip.split('.').map(o => parseInt(o, 10).toString(2).padStart(8, '0')).join('');
|
||||
}
|
||||
|
||||
function binaryToIp(b) {
|
||||
const parts = [];
|
||||
for (let i = 0; i < 32; i += 8) parts.push(parseInt(b.slice(i, i + 8), 2));
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
function cidrToMask(cidr) {
|
||||
return binaryToIp('1'.repeat(cidr) + '0'.repeat(32 - cidr));
|
||||
}
|
||||
|
||||
function maskToCidr(mask) {
|
||||
const b = ipToBinary(mask);
|
||||
if (/^1*0*$/.test(b)) return b.replace(/0+$/, '').length;
|
||||
return null;
|
||||
}
|
||||
|
||||
function calculate() {
|
||||
showInlineError(null);
|
||||
const ip = ipInput.value.trim();
|
||||
const cidrRaw = cidrInput.value.trim();
|
||||
|
||||
if (!isValidIP(ip)) { showInlineError('Bitte eine gültige IPv4-Adresse eingeben.'); return; }
|
||||
|
||||
let cidr, mask;
|
||||
if (cidrRaw.includes('.')) {
|
||||
if (!isValidIP(cidrRaw)) { showInlineError('Bitte eine gültige Subnetzmaske eingeben.'); return; }
|
||||
cidr = maskToCidr(cidrRaw);
|
||||
if (cidr === null) { showInlineError('Ungültige Subnetzmaske — muss eine kontinuierliche Folge von Einsen sein (z.B. 255.255.255.0).'); return; }
|
||||
mask = cidrRaw;
|
||||
} else {
|
||||
cidr = parseInt(cidrRaw, 10);
|
||||
if (isNaN(cidr) || cidr < 0 || cidr > 32) { showInlineError('Bitte einen gültigen CIDR-Wert (0–32) eingeben.'); return; }
|
||||
mask = cidrToMask(cidr);
|
||||
}
|
||||
|
||||
const ipBin = ipToBinary(ip);
|
||||
const maskBin = '1'.repeat(cidr) + '0'.repeat(32 - cidr);
|
||||
let netBin = '';
|
||||
for (let i = 0; i < 32; i++) netBin += (parseInt(ipBin[i]) & parseInt(maskBin[i])).toString();
|
||||
|
||||
const hostBits = 32 - cidr;
|
||||
const bcBin = netBin.slice(0, cidr) + '1'.repeat(hostBits);
|
||||
const netNum = parseInt(netBin, 2);
|
||||
const bcNum = parseInt(bcBin, 2);
|
||||
|
||||
let hosts, first, last;
|
||||
if (hostBits >= 2) {
|
||||
hosts = Math.pow(2, hostBits) - 2;
|
||||
first = binaryToIp((netNum + 1).toString(2).padStart(32, '0'));
|
||||
last = binaryToIp((bcNum - 1).toString(2).padStart(32, '0'));
|
||||
} else if (cidr === 31) {
|
||||
hosts = 2; first = binaryToIp(netBin); last = binaryToIp(bcBin);
|
||||
} else {
|
||||
hosts = 1; first = binaryToIp(netBin); last = binaryToIp(netBin);
|
||||
}
|
||||
|
||||
document.getElementById('network-address').textContent = binaryToIp(netBin);
|
||||
document.getElementById('broadcast-address').textContent = binaryToIp(bcBin);
|
||||
document.getElementById('subnet-mask').textContent = mask;
|
||||
document.getElementById('host-count').textContent = hosts.toLocaleString();
|
||||
document.getElementById('first-host').textContent = first;
|
||||
document.getElementById('last-host').textContent = last;
|
||||
|
||||
resultsEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
form.addEventListener('submit', e => { e.preventDefault(); calculate(); });
|
||||
|
||||
document.querySelectorAll('.example-link').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
ipInput.value = link.dataset.ip;
|
||||
cidrInput.value = link.dataset.cidr;
|
||||
calculate();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { API, setupCopyBtn, showError } from '../shared.js';
|
||||
|
||||
export const page = {
|
||||
title: 'WHOIS Lookup',
|
||||
|
||||
template: () => `
|
||||
<div class="container mx-auto max-w-5xl 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-8 text-center text-gradient">WHOIS Lookup</h1>
|
||||
|
||||
<div class="p-6 glass-card rounded-xl">
|
||||
<div class="flex flex-col sm:flex-row gap-3 mb-6">
|
||||
<input type="text" id="whois-query-input" placeholder="Enter domain or IP (e.g., google.com or 8.8.8.8)"
|
||||
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="whois-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 WHOIS
|
||||
</button>
|
||||
</div>
|
||||
<div id="whois-lookup-error" class="hidden mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-400 text-sm"></div>
|
||||
|
||||
<div id="whois-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-purple-300 flex items-center gap-2">
|
||||
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div>
|
||||
WHOIS Results for: <span id="whois-lookup-query" class="font-mono text-purple-400 ml-1"></span>
|
||||
</h3>
|
||||
<button id="copy-whois-btn" class="copy-btn">copy</button>
|
||||
</div>
|
||||
<div id="whois-lookup-loader" class="loader hidden mb-4"></div>
|
||||
<pre id="whois-lookup-output" class="result-pre"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="global-error" class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg"></div>
|
||||
</div>`,
|
||||
|
||||
async init(search) {
|
||||
const input = document.getElementById('whois-query-input');
|
||||
const btn = document.getElementById('whois-lookup-button');
|
||||
const errorEl = document.getElementById('whois-lookup-error');
|
||||
const section = document.getElementById('whois-lookup-results-section');
|
||||
const queryEl = document.getElementById('whois-lookup-query');
|
||||
const loader = document.getElementById('whois-lookup-loader');
|
||||
const output = document.getElementById('whois-lookup-output');
|
||||
const copyBtn = document.getElementById('copy-whois-btn');
|
||||
|
||||
const syncBtn = () => { btn.disabled = !input.value.trim(); };
|
||||
input.addEventListener('input', syncBtn);
|
||||
|
||||
setupCopyBtn(copyBtn, () => output.textContent);
|
||||
|
||||
async function doLookup() {
|
||||
const query = input.value.trim();
|
||||
if (!query) return;
|
||||
|
||||
const url = new URL(location.href);
|
||||
url.searchParams.set('query', query);
|
||||
history.replaceState({}, '', url);
|
||||
|
||||
showError(errorEl, null);
|
||||
section.classList.remove('hidden');
|
||||
loader.classList.remove('hidden');
|
||||
output.textContent = '';
|
||||
queryEl.textContent = query;
|
||||
|
||||
try {
|
||||
const r = await fetch(`${API}/whois-lookup?query=${encodeURIComponent(query)}`);
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.success) throw new Error(data.error || `HTTP ${r.status}`);
|
||||
output.textContent = typeof data.result === 'string'
|
||||
? data.result
|
||||
: JSON.stringify(data.result, null, 2);
|
||||
} catch (err) {
|
||||
showError(errorEl, err.message);
|
||||
output.textContent = '';
|
||||
} finally {
|
||||
loader.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', doLookup);
|
||||
input.addEventListener('keypress', e => { if (e.key === 'Enter' && !btn.disabled) doLookup(); });
|
||||
|
||||
const params = new URLSearchParams(search);
|
||||
const q = params.get('query');
|
||||
if (q) { input.value = q; syncBtn(); doLookup(); }
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user