← back to Domain Sniper
dashboard.js
201 lines
#!/usr/bin/env node
// dashboard.js — live web viewer of the global newly-registered-domain firehose.
//
// Pulls Certificate Transparency log events (every TLS cert issued worldwide)
// from certstream.calidog.io and rebroadcasts to browser clients over WebSocket.
//
// CT is the closest free PUBLIC real-time stream to what aggregators see.
// They have a paid WHOIS query feed (~$3K–50K/mo, gated to accredited registrars).
// CT lags that by minutes but covers ~95% of newly-registered domains.
//
// Run: node dashboard.js
// Open: http://127.0.0.1:9895
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
const fs = require('fs');
const ctLogSource = require('./ct-source-ctlog');
const PORT = parseInt(process.env.DASH_PORT || '9895', 10);
const CERTSTREAM_URL = process.env.CERTSTREAM_URL || 'wss://certstream.calidog.io/';
// CT_SOURCE=ctlog → direct CT-log polling (default, since certstream is dead)
// CT_SOURCE=certstream → original WebSocket flow (kept as fallback)
const CT_SOURCE = process.env.CT_SOURCE || 'ctlog';
const CT_LOG = process.env.CT_LOG || 'argon';
const CT_LOGS = (process.env.CT_LOGS || 'argon,xenon,wyvern,sphinx,elephant,tiger').split(',').map((s) => s.trim()).filter(Boolean);
const CTLOG_POLL_MS = parseInt(process.env.CTLOG_POLL_MS || '5000', 10);
const CTLOG_BATCH = parseInt(process.env.CTLOG_BATCH || '32', 10);
const DATA_DIR = path.join(__dirname, 'data');
const HITS_LOG = path.join(DATA_DIR, 'dashboard-hits.jsonl');
const HOT_TLDS = new Set(['com', 'app', 'io', 'co', 'ai', 'dev', 'xyz', 'org', 'net']);
const MAX_STEM_LEN = 8;
const BRAND_KEYWORDS = (process.env.BRAND_KEYWORDS || [
'designerwallcoverings', 'philipperomano', 'venturacorridor', 'venturaclaw',
'starsofdesign', 'bubbesblock', 'wholivedthere', 'agentabrams',
'nationalpaperhangers', 'novasuede', 'callr', 'butlr',
'flockedwallpaper', 'grasscloth', 'glassbeaded',
].join(',')).split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
function classify(d) {
if (!d || d.startsWith('*.')) return { hot: false, brand: null };
const dl = d.toLowerCase().replace(/^\*\./, '');
const brand = BRAND_KEYWORDS.find(k => dl.includes(k)) || null;
const parts = dl.split('.');
if (parts.length < 2) return { hot: false, brand };
const tld = parts[parts.length - 1];
if (!HOT_TLDS.has(tld)) return { hot: false, brand };
const stem = parts[parts.length - 2];
if (stem.length > MAX_STEM_LEN || stem.length < 3) return { hot: false, brand };
if (/[-0-9]/.test(stem)) return { hot: false, brand };
if (!/[aeiou]/.test(stem) || !/[bcdfghjklmnpqrstvwxyz]/.test(stem)) return { hot: false, brand };
return { hot: true, brand };
}
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
app.get('/api/keywords', (_req, res) => res.json({ keywords: BRAND_KEYWORDS }));
app.get('/api/perlog', (_req, res) => {
const now = Date.now();
const out = {};
for (const [k, v] of Object.entries(perLog)) out[k] = { ...v, stale: v.lastSeenMs ? (now - v.lastSeenMs > 60_000) : false };
res.json({ stats, perLog: out });
});
// Liveness probe — 200 if dashboard is up AND at least one CT source has
// reported any event (tick or idle) within 90s. Otherwise 503 so pm2/launchd
// can auto-restart a wedged process. Returns JSON so the body is also useful
// for human eyeball checks. No external calls; cheap to poll.
app.get('/healthz', (_req, res) => {
const now = Date.now();
const entries = Object.entries(perLog).map(([name, v]) => ({
name,
ageSec: v.lastSeenMs ? Math.round((now - v.lastSeenMs) / 1000) : null,
}));
const fresh = entries.filter((e) => e.ageSec !== null && e.ageSec < 90);
const ok = fresh.length > 0;
const uptimeSec = Math.round((now - stats.startedAt) / 1000);
res.status(ok ? 200 : 503).json({
ok,
uptimeSec,
sourcesFresh: fresh.length,
sourcesTotal: entries.length,
sources: entries,
seen: stats.seen,
hits: stats.hot + stats.brand,
});
});
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const clients = new Set();
let stats = { seen: 0, hot: 0, brand: 0, startedAt: Date.now() };
const perLog = {}; // log_name -> { fetched, x509, precert, emitted, idle, lastSeenMs }
wss.on('connection', (ws) => {
clients.add(ws);
ws.send(JSON.stringify({ type: 'stats', ...stats }));
ws.on('close', () => clients.delete(ws));
});
function broadcast(obj) {
const msg = JSON.stringify(obj);
for (const ws of clients) if (ws.readyState === 1) ws.send(msg);
}
function appendHit(rec) {
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.appendFileSync(HITS_LOG, JSON.stringify(rec) + '\n');
}
function handleCertEvent(m) {
if (m.message_type !== 'certificate_update') return;
const all = (m.data && m.data.leaf_cert && m.data.leaf_cert.all_domains) || [];
for (const d of all) {
stats.seen++;
const c = classify(d);
if (c.brand) stats.brand++;
if (c.hot) stats.hot++;
if (c.hot || c.brand) {
const rec = { type: 'hit', ts: Date.now(), domain: d, hot: c.hot, brand: c.brand, source: m.data.log_source || 'certstream' };
broadcast(rec);
appendHit(rec);
}
}
}
let reconnectDelay = 1000;
function connectCertstream() {
console.log(`[${new Date().toISOString()}] CT_SOURCE=certstream — connecting to ${CERTSTREAM_URL}`);
const cs = new WebSocket(CERTSTREAM_URL, { handshakeTimeout: 15_000 });
cs.on('open', () => {
console.log(`[${new Date().toISOString()}] CertStream connected`);
reconnectDelay = 1000;
});
cs.on('message', (raw) => {
let m; try { m = JSON.parse(raw.toString()); } catch { return; }
handleCertEvent(m);
});
cs.on('close', (code) => {
console.error(`[${new Date().toISOString()}] CT closed (${code}) — reconnect in ${reconnectDelay}ms`);
setTimeout(connectCertstream, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
});
cs.on('error', (e) => console.error(`[${new Date().toISOString()}] CT error: ${e.message}`));
}
function connectCtLog() {
const opts = CT_LOGS.length > 1
? { logKeys: CT_LOGS, pollMs: CTLOG_POLL_MS, batchSize: CTLOG_BATCH }
: { logKey: CT_LOG, pollMs: CTLOG_POLL_MS, batchSize: CTLOG_BATCH };
console.log(`[${new Date().toISOString()}] CT_SOURCE=ctlog ${CT_LOGS.length > 1 ? 'multi=[' + CT_LOGS.join(',') + ']' : 'log=' + CT_LOG} poll=${CTLOG_POLL_MS}ms batch=${CTLOG_BATCH}`);
ctLogSource.start({
...opts,
onEvent: handleCertEvent,
onStatus: (st) => {
if (st.type === 'sth_err' || st.type === 'fetch_err' || st.type === 'error') {
console.error(`[${new Date().toISOString()}] ctlog ${st.type} on ${st.log || '?'}: ${st.msg || JSON.stringify(st)}`);
} else if (st.type === 'seeded') {
console.log(`[${new Date().toISOString()}] ctlog seeded ${st.log} at tree_size=${st.position}`);
} else if (st.type === 'resumed') {
console.log(`[${new Date().toISOString()}] ctlog resumed ${st.log} at cursor=${st.position}`);
} else if (st.type === 'tick' && st.log) {
const p = (perLog[st.log] = perLog[st.log] || { fetched: 0, x509: 0, precert: 0, emitted: 0, idle: 0, lastSeenMs: 0 });
if (st.stats) { p.fetched = st.stats.fetched; p.x509 = st.stats.x509; p.precert = st.stats.precert; p.emitted = st.stats.emitted; }
p.lastSeenMs = Date.now();
} else if (st.type === 'idle' && st.log) {
const p = (perLog[st.log] = perLog[st.log] || { fetched: 0, x509: 0, precert: 0, emitted: 0, idle: 0, lastSeenMs: 0 });
p.idle++;
p.lastSeenMs = Date.now();
}
},
});
}
function connectCT() {
if (CT_SOURCE === 'certstream') return connectCertstream();
return connectCtLog();
}
setInterval(() => {
const now = Date.now();
const perLogPayload = {};
for (const [k, v] of Object.entries(perLog)) {
perLogPayload[k] = { ...v, stale: v.lastSeenMs ? (now - v.lastSeenMs > 60_000) : false };
}
broadcast({ type: 'stats', ...stats, perLog: perLogPayload });
}, 2000);
server.listen(PORT, '127.0.0.1', () => {
console.log(`\n domain-sniper :: live dashboard`);
console.log(` open http://127.0.0.1:${PORT}\n`);
console.log(` data source: CertStream (closest free real-time analog of an aggregator's WHOIS feed)`);
console.log(` brand keywords: ${BRAND_KEYWORDS.join(', ')}\n`);
connectCT();
});
process.on('SIGINT', () => process.exit(0));