import { API, setupCopyBtn } from '../shared.js';
export const page = {
title: 'IP Info & Tools',
template: () => `
Your Digital Footprint
Location Details
Country: -
Region: -
City: -
Zip: -
Coords: -
Time: -
IP Address / Domain Lookup
Result for:
Geolocation
Country: -
Region: -
City: -
Zip: -
Coords: -
Time: -
Location Map
`,
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 = 'No rDNS records found.';
} else if (data?.error) {
if (listEl) listEl.innerHTML = '-';
if (errorEl) errorEl.textContent = data.error;
} else {
if (listEl) listEl.innerHTML = '-';
}
}
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: '© OpenStreetMap © CARTO',
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 =
`AS${asnNum}`;
} 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 = '-';
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 =
`AS${data.asn.number}`;
} 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 = `Error: ${data.error}`;
} 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 ${data.port} (${data.service}): ${lbl}`;
}
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; }
};
}
};