Skip to content

Commit bd62aae

Browse files
committed
Add prefix-lookup page
Add prefix-lookup page
1 parent 6fb60d3 commit bd62aae

1 file changed

Lines changed: 214 additions & 0 deletions

File tree

src/pages/prefix-lookup.astro

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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

Comments
 (0)