import { API, setupCopyBtn } from '../shared.js'; export const page = { title: 'IP Info & Tools', template: () => `

Your Digital Footprint

Your Public IP

Location Details

Network (ASN)

Hostname (rDNS)

Location Visualization

IP Address / Domain Lookup

`, 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; } }; } };