Files
MyIP/common/as-org-db.js
T
jason5ng32andClaude Opus 5.5 287c50fe1a Feat(datasets): one updater engine for every offline dataset
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>
2026-10-05 02:21:58 +08:00

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;
}