← back to Purelymail Fleet Viewer
server.js
175 lines
#!/usr/bin/env node
/* Purelymail fleet remediation — live web viewer (zero dependencies).
*
* node server.js # http://127.0.0.1:9779
*
* /api/status merges:
* - email-deliverability-agent/output/domain-health.json (live fleet state, updated by the DNS agent)
* - data/remediation-status.json (Fix A/B + receive-test progress)
*/
'use strict';
const http = require('http');
const fs = require('fs');
const os = require('os');
const path = require('path');
const PORT = 9779;
const HEALTH = path.join(os.homedir(), 'Projects/email-deliverability-agent/output/domain-health.json');
const STATUS = path.join(__dirname, 'data/remediation-status.json');
const readJSON = (p, fb) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fb; } };
function buildStatus() {
const health = readJSON(HEALTH, null);
const remediation = readJSON(STATUS, null);
return {
generatedAt: new Date().toISOString(),
healthFound: !!health,
health: health || { totals: {}, domains: [], dualSpfDomains: [], deadDomains: [], noRoutingDomains: [] },
remediation: remediation || { stage: 'unknown', fixA: {}, fixB: {}, receiveTest: {} },
};
}
// --- embed-reachability check ----------------------------------------------
// Server-side probe: is the site reachable, and does it allow being framed?
// (X-Frame-Options / CSP frame-ancestors). Cached 15 min so the table fills
// fast on reload and we don't hammer 200+ sites.
const embedCache = new Map();
const EMBED_TTL = 15 * 60 * 1000;
async function embedCheck(domain) {
const hit = embedCache.get(domain);
if (hit && Date.now() - hit.ts < EMBED_TTL) return hit.result;
let result;
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), 9000);
try {
const r = await fetch('https://' + domain + '/', {
method: 'GET', redirect: 'follow', signal: ac.signal,
headers: { 'User-Agent': 'Mozilla/5.0 (purelymail-fleet-viewer embed-check)' },
});
const xfo = (r.headers.get('x-frame-options') || '').toLowerCase();
const csp = (r.headers.get('content-security-policy') || '').toLowerCase();
const fa = (csp.match(/frame-ancestors([^;]*)/) || [])[1];
const blockedByXFO = xfo.includes('deny') || xfo.includes('sameorigin');
const blockedByCSP = fa != null && !fa.includes('*');
const reachable = r.status > 0 && r.status < 500;
result = {
domain, reachable, status: r.status,
embeddable: reachable && !blockedByXFO && !blockedByCSP,
reason: !reachable ? ('HTTP ' + r.status)
: blockedByXFO ? 'blocks embedding (X-Frame-Options)'
: blockedByCSP ? 'blocks embedding (CSP frame-ancestors)' : 'ok',
};
} catch (e) {
result = { domain, reachable: false, status: 0, embeddable: false,
reason: e.name === 'AbortError' ? 'timeout / no response' : (e.cause && e.cause.code) || e.code || 'unreachable' };
} finally { clearTimeout(timer); }
embedCache.set(domain, { ts: Date.now(), result });
return result;
}
// --- website screenshot service --------------------------------------------
// ~half the fleet blocks iframe embedding (X-Frame-Options / CSP). Screenshots
// always render, so the grid + table previews show every domain reliably.
// Captured via headless Chromium, disk-cached 24h, concurrency-limited to 3.
const { chromium } = require('playwright');
const SHOT_DIR = path.join(__dirname, 'data/shots');
const SHOT_TTL = 24 * 60 * 60 * 1000;
try { fs.mkdirSync(SHOT_DIR, { recursive: true }); } catch {}
let browserP = null;
const getBrowser = () => (browserP = browserP || chromium.launch({ args: ['--no-sandbox'] }));
let active = 0;
const shotQueue = [];
function runQueue() {
while (active < 3 && shotQueue.length) {
const job = shotQueue.shift();
active++;
captureShot(job.domain).then(job.resolve, () => job.resolve({ ok: false }))
.finally(() => { active--; runQueue(); });
}
}
const queueShot = domain => new Promise(resolve => { shotQueue.push({ domain, resolve }); runQueue(); });
async function captureShot(domain) {
const file = path.join(SHOT_DIR, domain + '.png');
try {
const st = fs.statSync(file);
if (Date.now() - st.mtimeMs < SHOT_TTL && st.size > 0) return { ok: true, file };
} catch {}
let page;
try {
page = await (await getBrowser()).newPage({ viewport: { width: 1280, height: 800 } });
await page.goto('https://' + domain + '/', { waitUntil: 'domcontentloaded', timeout: 15000 });
await page.waitForTimeout(1800);
await page.screenshot({ path: file, clip: { x: 0, y: 0, width: 1280, height: 800 } });
return { ok: true, file };
} catch (e) {
return { ok: false, error: e.message };
} finally {
if (page) await page.close().catch(() => {});
}
}
// 404-guard: never serve editor/scratch/backup detritus (.bak, .pre-*, etc.)
// even if a stray file slips into a static-served directory. Matched against
// the URL pathname BEFORE any file lookup.
const JUNK_PATH_RE = /\.(bak(\..*)?|pre-[^/]*|orig|rej|swp|swo)$|~$/i;
const server = http.createServer((req, res) => {
const pathname = (req.url || '').split('?')[0];
if (JUNK_PATH_RE.test(pathname)) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('not found');
return;
}
if (req.url.startsWith('/api/shot')) {
const domain = (new URL(req.url, 'http://x').searchParams.get('domain') || '')
.replace(/[^a-z0-9.\-]/gi, '');
if (!domain) { res.writeHead(400); res.end(); return; }
queueShot(domain).then(r => {
if (r && r.ok) {
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'max-age=3600' });
fs.createReadStream(r.file).pipe(res);
} else {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (r && r.error) || 'capture failed' }));
}
});
return;
}
if (req.url === '/api/status') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(buildStatus()));
return;
}
if (req.url.startsWith('/api/embedcheck')) {
const domain = (new URL(req.url, 'http://x').searchParams.get('domain') || '')
.replace(/[^a-z0-9.\-]/gi, '');
if (!domain) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end('{}'); return; }
embedCheck(domain).then(r => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(r));
}).catch(() => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ domain, reachable: false, embeddable: false, reason: 'check failed' }));
});
return;
}
try {
const html = fs.readFileSync(path.join(__dirname, 'public/index.html'), 'utf8');
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
} catch {
res.writeHead(404); res.end('index.html missing');
}
});
// Bind 0.0.0.0 so the viewer is reachable over Steve's tailnet (e.g. from the
// MacBook), not just localhost on this machine. Non-sensitive status data.
server.listen(PORT, '0.0.0.0', () => {
console.log(`purelymail fleet viewer -> http://0.0.0.0:${PORT} (tailnet: 100.65.187.120:${PORT})`);
});