|
| 1 | +--- |
| 2 | +import BaseLayout from '../layouts/BaseLayout.astro'; |
| 3 | +import releases from '../../assets/data/releases.json'; |
| 4 | +
|
| 5 | +// Current release drives which monthly shard set we read. |
| 6 | +const release = releases.current_release; |
| 7 | +
|
| 8 | +// Per-/8 Parquet shards live in a dedicated data repo, served via jsDelivr |
| 9 | +// (CORS + byte caching). One ~50-300 KB file is fetched per lookup. |
| 10 | +const dataBase = 'https://cdn.jsdelivr.net/gh/ProdIPData/prefix-data@main'; |
| 11 | +
|
| 12 | +const breadcrumb = { |
| 13 | + '@context': 'https://schema.org', |
| 14 | + '@type': 'BreadcrumbList', |
| 15 | + itemListElement: [ |
| 16 | + { '@type': 'ListItem', position: 1, name: 'Home', item: 'https://geoiplocations.com/' }, |
| 17 | + { '@type': 'ListItem', position: 2, name: 'Prefix lookup', item: 'https://geoiplocations.com/prefix-lookup.html' }, |
| 18 | + ], |
| 19 | +}; |
| 20 | +--- |
| 21 | +<BaseLayout |
| 22 | + title="IP Prefix Lookup — GeoIP, ASN & Registry Details | ProdIPData" |
| 23 | + description="Look up any IPv4 address or /24 prefix and see its published GeoIP location, ASN, WHOIS ownership, RIR registry and risk attributes — read straight from the monthly Parquet release in your browser." |
| 24 | + ogTitle="IP Prefix Lookup" |
| 25 | + canonicalPath="/prefix-lookup.html" |
| 26 | + page="prefix-lookup" |
| 27 | +> |
| 28 | + <script slot="head" type="application/ld+json" is:inline set:html={JSON.stringify(breadcrumb)} /> |
| 29 | + |
| 30 | + <main class="container lookup"> |
| 31 | + <section class="lookup-hero"> |
| 32 | + <h1>Prefix lookup</h1> |
| 33 | + <p class="lookup-sub"> |
| 34 | + Enter an IPv4 address or a <code>/24</code> prefix to see its published attributes for the |
| 35 | + <strong>{release}</strong> release — geolocation, ASN, ownership, registry and risk signals. |
| 36 | + Everything is read directly from the monthly Parquet data in your browser; nothing is sent to a server. |
| 37 | + </p> |
| 38 | + |
| 39 | + <form id="lookupForm" class="lookup-form glass" autocomplete="off"> |
| 40 | + <input |
| 41 | + id="lookupInput" |
| 42 | + class="lookup-input" |
| 43 | + type="text" |
| 44 | + inputmode="numeric" |
| 45 | + placeholder="e.g. 8.8.8.8 or 8.8.8.0/24" |
| 46 | + aria-label="IPv4 address or /24 prefix" |
| 47 | + /> |
| 48 | + <button class="btn btn-secondary" type="submit">Look up</button> |
| 49 | + </form> |
| 50 | + <div id="lookupStatus" class="lookup-status" role="status" aria-live="polite"></div> |
| 51 | + </section> |
| 52 | + |
| 53 | + <section id="lookupResult" class="lookup-result" hidden></section> |
| 54 | + |
| 55 | + <p class="lookup-note muted"> |
| 56 | + Granularity is one record per <code>/24</code> block. A full IP is resolved to its containing |
| 57 | + <code>/24</code>. Unrouted or unallocated space returns no record. Data source: |
| 58 | + the current monthly <a href={`./downloads.html`}>release</a>. |
| 59 | + </p> |
| 60 | + </main> |
| 61 | + |
| 62 | + <div id="lookupConfig" data-release={release} data-base={dataBase} hidden></div> |
| 63 | + |
| 64 | + <style is:inline> |
| 65 | + .lookup { padding: 2.5rem 0 4rem; max-width: 920px; } |
| 66 | + .lookup-hero h1 { margin: 0 0 .5rem; } |
| 67 | + .lookup-sub { color: var(--color-text-secondary, #5b6b7f); line-height: 1.6; margin-bottom: 1.5rem; } |
| 68 | + .lookup-form { display: flex; gap: .6rem; padding: .8rem; border-radius: 14px; align-items: center; flex-wrap: wrap; } |
| 69 | + .lookup-input { |
| 70 | + flex: 1 1 320px; min-width: 220px; font: inherit; font-size: 1.05rem; |
| 71 | + padding: .7rem .9rem; border-radius: 10px; border: 1px solid rgba(120,140,170,.35); |
| 72 | + background: rgba(255,255,255,.7); color: inherit; |
| 73 | + } |
| 74 | + .lookup-input:focus { outline: 2px solid #2f6fed; outline-offset: 1px; } |
| 75 | + .lookup-form .btn { font-size: 1.05rem; padding: .7rem 1.3rem; } |
| 76 | + .lookup-status { margin-top: .7rem; min-height: 1.3rem; font-size: .95rem; color: var(--color-text-secondary, #5b6b7f); } |
| 77 | + .lookup-status.err { color: #b4232a; } |
| 78 | + .lookup-result { margin-top: 1.8rem; display: grid; gap: 1rem; } |
| 79 | + .lk-card { border: 1px solid rgba(120,140,170,.22); border-radius: 14px; padding: 1rem 1.2rem; background: rgba(255,255,255,.55); } |
| 80 | + .lk-card h2 { font-size: .78rem; letter-spacing: .09em; text-transform: uppercase; color: #2f6fed; margin: 0 0 .7rem; } |
| 81 | + .lk-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .55rem 1.6rem; } |
| 82 | + @media (max-width: 620px) { .lk-grid { grid-template-columns: 1fr; } } |
| 83 | + .lk-row { display: flex; justify-content: space-between; gap: 1rem; border-bottom: 1px dashed rgba(120,140,170,.2); padding-bottom: .4rem; } |
| 84 | + .lk-row .k { color: var(--color-text-secondary, #5b6b7f); } |
| 85 | + .lk-row .v { font-weight: 500; text-align: right; word-break: break-word; } |
| 86 | + .lk-head { font-size: 1.5rem; font-weight: 600; } |
| 87 | + .lk-badge { display: inline-block; font-size: .8rem; font-weight: 600; padding: .15rem .6rem; border-radius: 999px; margin-left: .5rem; vertical-align: middle; } |
| 88 | + .lk-badge.ok { background: rgba(45,180,120,.16); color: #157a4f; } |
| 89 | + .lk-badge.warn { background: rgba(214,90,40,.16); color: #b4232a; } |
| 90 | + .lookup-note { margin-top: 2rem; font-size: .9rem; } |
| 91 | + .lk-spin { display:inline-block; width:14px; height:14px; border:2px solid rgba(47,111,237,.3); border-top-color:#2f6fed; border-radius:50%; animation: lkspin .7s linear infinite; vertical-align:-2px; margin-right:.5rem; } |
| 92 | + @keyframes lkspin { to { transform: rotate(360deg); } } |
| 93 | + </style> |
| 94 | + |
| 95 | + <script is:inline type="module"> |
| 96 | + import { parquetReadObjects } from 'https://esm.sh/hyparquet@1'; |
| 97 | + import { compressors } from 'https://esm.sh/hyparquet-compressors@1'; |
| 98 | + |
| 99 | + const cfg = document.getElementById('lookupConfig').dataset; |
| 100 | + const RELEASE = cfg.release; |
| 101 | + const BASE = cfg.base; |
| 102 | + |
| 103 | + const form = document.getElementById('lookupForm'); |
| 104 | + const input = document.getElementById('lookupInput'); |
| 105 | + const statusEl = document.getElementById('lookupStatus'); |
| 106 | + const resultEl = document.getElementById('lookupResult'); |
| 107 | + |
| 108 | + const shardCache = new Map(); // octet -> Map(prefix -> row) |
| 109 | + |
| 110 | + function setStatus(msg, isErr) { |
| 111 | + statusEl.innerHTML = msg; |
| 112 | + statusEl.classList.toggle('err', !!isErr); |
| 113 | + } |
| 114 | + |
| 115 | + // Accept "8.8.8.8", "8.8.8.0/24", "8.8.8" -> { octet, prefix:"8.8.8.0/24" } |
| 116 | + function toPrefix24(raw) { |
| 117 | + const s = String(raw || '').trim().split('/')[0].trim(); |
| 118 | + const m = s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\.(\d{1,3}))?$/); |
| 119 | + if (!m) return null; |
| 120 | + const o = [m[1], m[2], m[3]].map(Number); |
| 121 | + if (o.some((x) => x < 0 || x > 255)) return null; |
| 122 | + return { octet: o[0], prefix: `${o[0]}.${o[1]}.${o[2]}.0/24` }; |
| 123 | + } |
| 124 | + |
| 125 | + function val(v) { |
| 126 | + if (v === null || v === undefined || v === '') return null; |
| 127 | + if (v instanceof Uint8Array) return new TextDecoder().decode(v); |
| 128 | + return v; |
| 129 | + } |
| 130 | + |
| 131 | + async function loadShard(octet) { |
| 132 | + if (shardCache.has(octet)) return shardCache.get(octet); |
| 133 | + const url = `${BASE}/${RELEASE}/${octet}.parquet`; |
| 134 | + const res = await fetch(url); |
| 135 | + if (res.status === 404) { shardCache.set(octet, null); return null; } // octet not allocated |
| 136 | + if (!res.ok) throw new Error(`fetch ${res.status}`); |
| 137 | + const ab = await res.arrayBuffer(); |
| 138 | + const file = { byteLength: ab.byteLength, slice: (a, b) => ab.slice(a, b ?? ab.byteLength) }; |
| 139 | + const rows = await parquetReadObjects({ file, compressors }); |
| 140 | + const map = new Map(); |
| 141 | + for (const r of rows) map.set(val(r.IP24Prefix), r); |
| 142 | + shardCache.set(octet, map); |
| 143 | + return map; |
| 144 | + } |
| 145 | + |
| 146 | + const FIELDS = [ |
| 147 | + ['Location', [ |
| 148 | + ['CountryCode', 'Country'], ['ContinentCode', 'Continent'], ['IsInEU', 'In EU'], |
| 149 | + ['RegionName', 'Region'], ['CityName', 'City'], |
| 150 | + ['Latitude', 'Latitude'], ['Longitude', 'Longitude'], ['TimeZone', 'Time zone'], |
| 151 | + ]], |
| 152 | + ['Network / ASN', [ |
| 153 | + ['ASNID', 'ASN'], ['ASNName', 'ASN name'], ['ASNOrg', 'ASN org'], |
| 154 | + ['ASNDomain', 'ASN domain'], ['ASNType', 'ASN type'], |
| 155 | + ]], |
| 156 | + ['Ownership (WHOIS)', [ |
| 157 | + ['Company', 'Company'], ['CompanyCountry', 'Company country'], |
| 158 | + ['WhoisNetName', 'Net name'], ['AbuseContact', 'Abuse contact'], |
| 159 | + ]], |
| 160 | + ['Registry', [ |
| 161 | + ['RIR', 'RIR'], ['RIRAllocationDate', 'Allocated'], ['RIRStatus', 'Status'], |
| 162 | + ]], |
| 163 | + ['Risk signals', [ |
| 164 | + ['HasC2', 'C2 hosting'], ['C2Malwares', 'C2 malware'], |
| 165 | + ['IsTor', 'Tor'], ['Anonymization', 'Anonymization'], |
| 166 | + ]], |
| 167 | + ]; |
| 168 | + |
| 169 | + const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); |
| 170 | + |
| 171 | + function render(prefix, row) { |
| 172 | + const head = `<div class="lk-card"><div class="lk-head">${esc(prefix)}</div> |
| 173 | + <div class="muted" style="margin-top:.2rem">Snapshot ${esc(val(row.snapshot_month) || RELEASE)}</div></div>`; |
| 174 | + const cards = FIELDS.map(([title, fields]) => { |
| 175 | + const rows = fields.map(([key, label]) => { |
| 176 | + let v = val(row[key]); |
| 177 | + if (v === null) return ''; |
| 178 | + if (key === 'IsInEU' || key === 'IsTor') v = (String(v) === 'true' || v === true || v === 1) ? 'Yes' : 'No'; |
| 179 | + if (key === 'HasC2') { |
| 180 | + const on = (String(v) === 'true' || v === true || v === 1); |
| 181 | + return `<div class="lk-row"><span class="k">${label}</span><span class="v">${on ? '<span class="lk-badge warn">Yes</span>' : 'No'}</span></div>`; |
| 182 | + } |
| 183 | + return `<div class="lk-row"><span class="k">${label}</span><span class="v">${esc(v)}</span></div>`; |
| 184 | + }).join(''); |
| 185 | + return rows ? `<div class="lk-card"><h2>${title}</h2><div class="lk-grid">${rows}</div></div>` : ''; |
| 186 | + }).join(''); |
| 187 | + resultEl.innerHTML = head + cards; |
| 188 | + resultEl.hidden = false; |
| 189 | + } |
| 190 | + |
| 191 | + async function doLookup(e) { |
| 192 | + if (e) e.preventDefault(); |
| 193 | + resultEl.hidden = true; |
| 194 | + const parsed = toPrefix24(input.value); |
| 195 | + if (!parsed) { setStatus('Enter a valid IPv4 address or /24 prefix (e.g. 8.8.8.8).', true); return; } |
| 196 | + setStatus(`<span class="lk-spin"></span>Looking up ${esc(parsed.prefix)} …`); |
| 197 | + try { |
| 198 | + const map = await loadShard(parsed.octet); |
| 199 | + if (!map) { setStatus(`No data for ${esc(parsed.prefix)} — that block is unrouted or unallocated in the ${esc(RELEASE)} release.`); return; } |
| 200 | + const row = map.get(parsed.prefix); |
| 201 | + if (!row) { setStatus(`No record for ${esc(parsed.prefix)} — this /24 isn't present in the ${esc(RELEASE)} release (likely unrouted).`); return; } |
| 202 | + setStatus(`Result for ${esc(parsed.prefix)}`); |
| 203 | + render(parsed.prefix, row); |
| 204 | + } catch (err) { |
| 205 | + setStatus(`Lookup failed: ${esc(err.message || err)}. The data may not be published yet.`, true); |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + form.addEventListener('submit', doLookup); |
| 210 | + // Deep link support: /prefix-lookup.html?q=8.8.8.8 |
| 211 | + const q = new URLSearchParams(location.search).get('q'); |
| 212 | + if (q) { input.value = q; doLookup(); } |
| 213 | + </script> |
| 214 | +</BaseLayout> |
0 commit comments