137 lines
5.3 KiB
JavaScript
137 lines
5.3 KiB
JavaScript
// Länderzuordnung per DB-IP Lite (country, CSV: start_ip,end_ip,land). Die Datei wird einmal beim
|
||
// Start geladen (IPv4 als Uint32, IPv6 als 2×64 Bit in BigUint64Array) und per Binärsuche abgefragt.
|
||
import { readFileSync } from 'node:fs';
|
||
import net from 'node:net';
|
||
|
||
const MASK64 = (1n << 64n) - 1n;
|
||
|
||
function parseV4(s) {
|
||
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
|
||
if (!m) return -1;
|
||
let n = 0;
|
||
for (let i = 1; i <= 4; i++) {
|
||
const o = Number(m[i]);
|
||
if (o > 255) return -1;
|
||
n = n * 256 + o;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
// IPv6 ⇒ 128-Bit-BigInt oder null (auch mit angehängter IPv4-Schreibweise).
|
||
function parseV6(s) {
|
||
if (net.isIPv6(s) === false) return null;
|
||
s = s.split('%')[0];
|
||
const dot = s.lastIndexOf('.');
|
||
if (dot !== -1) {
|
||
const v4 = parseV4(s.slice(s.lastIndexOf(':') + 1));
|
||
if (v4 < 0) return null;
|
||
s = s.slice(0, s.lastIndexOf(':') + 1) + Math.floor(v4 / 65536).toString(16) + ':' + (v4 % 65536).toString(16);
|
||
}
|
||
const [head, tail] = s.split('::');
|
||
const h = head ? head.split(':') : [];
|
||
const t = tail === undefined ? [] : (tail ? tail.split(':') : []);
|
||
const fill = tail === undefined ? 0 : 8 - h.length - t.length;
|
||
const groups = [...h, ...Array(Math.max(fill, 0)).fill('0'), ...t];
|
||
if (groups.length !== 8) return null;
|
||
let n = 0n;
|
||
for (const g of groups) n = (n << 16n) | BigInt(parseInt(g, 16));
|
||
return n;
|
||
}
|
||
|
||
const pack = (c) => (c.length === 2 && c !== 'ZZ' && /^[A-Za-z]{2}$/.test(c) ? (c.toUpperCase().charCodeAt(0) << 8) | c.toUpperCase().charCodeAt(1) : 0);
|
||
const unpack = (n) => (n ? String.fromCharCode(n >> 8, n & 255) : '');
|
||
|
||
function sortedBy(n, cmp) {
|
||
for (let i = 1; i < n; i++) if (cmp(i - 1, i) > 0) return false;
|
||
return true;
|
||
}
|
||
|
||
export function loadGeo(file) {
|
||
const none = { lookup: () => '', size: 0 };
|
||
let text;
|
||
try { text = readFileSync(file, 'utf8'); } catch (e) {
|
||
console.warn(`Analytics: Länderdatei ${file} nicht lesbar (${e.code || e.message}); Länder bleiben leer.`);
|
||
return none;
|
||
}
|
||
// Obergrenze der Zeilen für die Vorab-Reservierung
|
||
let cap = 1;
|
||
for (let i = text.indexOf('\n'); i !== -1; i = text.indexOf('\n', i + 1)) cap++;
|
||
let n4 = 0;
|
||
let n6 = 0;
|
||
const s4 = new Uint32Array(cap); const e4 = new Uint32Array(cap); const c4 = new Uint16Array(cap);
|
||
const sh = new BigUint64Array(cap); const sl = new BigUint64Array(cap);
|
||
const eh = new BigUint64Array(cap); const el = new BigUint64Array(cap); const c6 = new Uint16Array(cap);
|
||
let pos = 0;
|
||
while (pos < text.length) {
|
||
let nl = text.indexOf('\n', pos);
|
||
if (nl === -1) nl = text.length;
|
||
const line = text.slice(pos, nl).replace(/["\r]/g, '').trim();
|
||
pos = nl + 1;
|
||
if (!line) continue;
|
||
const p = line.split(',');
|
||
if (p.length < 3) continue;
|
||
const a = p[0].trim(); const b = p[1].trim(); const c = pack(p[2].trim());
|
||
if (a.includes(':')) {
|
||
const x = parseV6(a); const y = parseV6(b);
|
||
if (x === null || y === null || y < x) continue;
|
||
sh[n6] = x >> 64n; sl[n6] = x & MASK64; eh[n6] = y >> 64n; el[n6] = y & MASK64; c6[n6++] = c;
|
||
} else {
|
||
const x = parseV4(a); const y = parseV4(b);
|
||
if (x < 0 || y < x) continue;
|
||
s4[n4] = x; e4[n4] = y; c4[n4++] = c;
|
||
}
|
||
}
|
||
text = null;
|
||
|
||
// Falls die Datei nicht sortiert ist: Indexsortierung
|
||
let S4 = s4.slice(0, n4); let E4 = e4.slice(0, n4); let C4 = c4.slice(0, n4);
|
||
if (!sortedBy(n4, (i, j) => S4[i] - S4[j])) {
|
||
const idx = Array.from({ length: n4 }, (_, i) => i).sort((i, j) => S4[i] - S4[j]);
|
||
S4 = Uint32Array.from(idx, (i) => s4[i]); E4 = Uint32Array.from(idx, (i) => e4[i]); C4 = Uint16Array.from(idx, (i) => c4[i]);
|
||
}
|
||
let SH = sh.slice(0, n6); let SL = sl.slice(0, n6); let EH = eh.slice(0, n6); let EL = el.slice(0, n6); let C6 = c6.slice(0, n6);
|
||
const cmp6 = (i, j) => (SH[i] !== SH[j] ? (SH[i] < SH[j] ? -1 : 1) : (SL[i] === SL[j] ? 0 : (SL[i] < SL[j] ? -1 : 1)));
|
||
if (!sortedBy(n6, cmp6)) {
|
||
const idx = Array.from({ length: n6 }, (_, i) => i).sort(cmp6);
|
||
const pick = (A, T) => T.from(idx, (i) => A[i]);
|
||
const [a, b, c, d, e] = [pick(SH, BigUint64Array), pick(SL, BigUint64Array), pick(EH, BigUint64Array), pick(EL, BigUint64Array), pick(C6, Uint16Array)];
|
||
SH = a; SL = b; EH = c; EL = d; C6 = e;
|
||
}
|
||
|
||
const look4 = (v) => {
|
||
let lo = 0; let hi = n4 - 1; let best = -1;
|
||
while (lo <= hi) {
|
||
const mid = (lo + hi) >>> 1;
|
||
if (S4[mid] <= v) { best = mid; lo = mid + 1; } else hi = mid - 1;
|
||
}
|
||
return best >= 0 && v <= E4[best] ? unpack(C4[best]) : '';
|
||
};
|
||
const look6 = (h, l) => {
|
||
let lo = 0; let hi = n6 - 1; let best = -1;
|
||
while (lo <= hi) {
|
||
const mid = (lo + hi) >>> 1;
|
||
if (SH[mid] < h || (SH[mid] === h && SL[mid] <= l)) { best = mid; lo = mid + 1; } else hi = mid - 1;
|
||
}
|
||
if (best < 0) return '';
|
||
const inside = h < EH[best] || (h === EH[best] && l <= EL[best]);
|
||
return inside ? unpack(C6[best]) : '';
|
||
};
|
||
|
||
return {
|
||
size: n4 + n6,
|
||
lookup(ip) {
|
||
if (typeof ip !== 'string') return '';
|
||
ip = ip.trim();
|
||
if (!ip.includes(':')) {
|
||
const v = parseV4(ip);
|
||
return v < 0 ? '' : look4(v);
|
||
}
|
||
const v = parseV6(ip);
|
||
if (v === null) return '';
|
||
const h = v >> 64n; const l = v & MASK64;
|
||
if (h === 0n && (l >> 32n) === 0xffffn) return look4(Number(l & 0xffffffffn)); // ::ffff:a.b.c.d
|
||
return look6(h, l);
|
||
},
|
||
};
|
||
}
|