mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
MaxMind and CAIDA each had their own updater with its own lock, state file, publish step, boot download and timer. One engine (common/dataset-updater.js) now owns all of that, and each dataset is a row in common/datasets.js that holds only its own logic: find the newest remote version, fetch or distill it, validate, reload. - Locks: proper-lockfile, a heartbeat lock shared by every process, so a crashed holder's lock goes stale on its own; a booting server waits for a live holder and loads what it published. - State: one file per dataset, written with write-file-atomic; the old .maxmind-update-state.json / .caida-update-state.json are read once, so an upgrade doesn't re-download. - Schedule: croner at a fixed time (DATASET_UPDATE_CRON, default 04:30 local) instead of 24 h from process start, plus a catch-up run at boot for a check a downtime skipped. DATASET_AUTO_UPDATE (default on) replaces MAXMIND_AUTO_UPDATE / CAIDA_AUTO_UPDATE, which still apply when it is unset. - Reload: the engine watches every dataset's files, so a publish by another process or a hand-placed .mmdb is picked up; MaxMind's own watcher goes. MaxMind's City and ASN editions now publish together under one version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
94 lines
3.4 KiB
JavaScript
94 lines
3.4 KiB
JavaScript
// Local CAIDA as2org lookup. Snapshot file is auto-downloaded by the
|
|
// dataset updater (common/datasets.js); this module just parses and serves.
|
|
//
|
|
// We use CAIDA's pipe-delimited TXT (~12MB) rather than the equivalent
|
|
// JSONL (~28MB): identical content but split('|') beats JSON.parse per
|
|
// line by ~40%.
|
|
//
|
|
// Source: https://publicdata.caida.org/datasets/as-organizations/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import logger from './logger.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
export const AS_ORG_DB_DIR = path.join(__dirname, 'as-org-db');
|
|
|
|
// Canonical filename the updater writes to. Manual downloads can use any
|
|
// *.txt name — findSnapshot picks the newest by mtime regardless.
|
|
export const AS_ORG_FILE = 'as-org2info.txt';
|
|
|
|
const asnToOrgName = new Map();
|
|
let loadedFrom = null;
|
|
|
|
function findSnapshot() {
|
|
if (!fs.existsSync(AS_ORG_DB_DIR)) return null;
|
|
const txts = fs.readdirSync(AS_ORG_DB_DIR).filter(f => f.endsWith('.txt'));
|
|
if (txts.length === 0) return null;
|
|
const withMtime = txts.map(f => {
|
|
const full = path.join(AS_ORG_DB_DIR, f);
|
|
return { full, mtimeMs: fs.statSync(full).mtimeMs };
|
|
});
|
|
withMtime.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
return withMtime[0].full;
|
|
}
|
|
|
|
// CAIDA TXT has two sections: org records (5 fields) and ASN records (6
|
|
// fields). Orgs may appear after ASNs in the stream, so we collect ASN
|
|
// rows into `pending` first and resolve them after a full scan.
|
|
function parsePipeText(text) {
|
|
const orgs = new Map(); // org_id → name
|
|
const pending = []; // [{asn, org_id}]
|
|
for (const rawLine of text.split('\n')) {
|
|
const line = rawLine.trim();
|
|
if (!line || line.startsWith('#')) continue;
|
|
const parts = line.split('|');
|
|
if (parts.length === 5) {
|
|
// org_id | changed | name | country | source
|
|
if (parts[0] && parts[2]) orgs.set(parts[0], parts[2]);
|
|
} else if (parts.length === 6) {
|
|
// aut | changed | aut_name | org_id | opaque_id | source
|
|
const asn = Number(parts[0]);
|
|
if (asn && parts[3]) pending.push({ asn, org_id: parts[3] });
|
|
}
|
|
}
|
|
for (const { asn, org_id } of pending) {
|
|
const name = orgs.get(org_id);
|
|
if (name) asnToOrgName.set(asn, name);
|
|
}
|
|
}
|
|
|
|
function loadDatabase() {
|
|
const filePath = findSnapshot();
|
|
if (!filePath) {
|
|
logger.warn({ dir: AS_ORG_DB_DIR }, '⚠️ CAIDA as2org snapshot not found; ASN org-name lookups will fall back to RIPEstat');
|
|
asnToOrgName.clear();
|
|
loadedFrom = null;
|
|
return;
|
|
}
|
|
const start = Date.now();
|
|
try {
|
|
const text = fs.readFileSync(filePath, 'utf8');
|
|
asnToOrgName.clear();
|
|
parsePipeText(text);
|
|
loadedFrom = path.basename(filePath);
|
|
logger.info(`📦 CAIDA as2org loaded (${loadedFrom}) — ${asnToOrgName.size} ASNs in ${Date.now() - start}ms`);
|
|
} catch (error) {
|
|
logger.warn({ err: error, path: filePath }, '⚠️ Failed to parse CAIDA as2org snapshot');
|
|
}
|
|
}
|
|
|
|
loadDatabase();
|
|
|
|
/** Reload the snapshot after the dataset updater publishes a fresh file. */
|
|
export function reloadAsOrgDatabase(reason = 'reload') {
|
|
logger.info(`🔄 Reloading CAIDA as2org snapshot (${reason})`);
|
|
loadDatabase();
|
|
}
|
|
|
|
/** Org name for an ASN, or null when the snapshot doesn't have it. */
|
|
export function lookupAsOrgName(asn) {
|
|
return asnToOrgName.get(Number(asn)) || null;
|
|
}
|