Files
Webklar-Kundenbereich/analytics/src/geo.js
T
2026-10-01 09:30:58 +00:00

137 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
},
};
}