#!/usr/bin/env node /** * Lists every Tor hidden-service hostname + nginx loopback URL, and checks * that each loopback port returns HTTP (proves nginx → Next are up for that vhost). * * node scripts/onion-status.cjs * npm run onions:status * * Reading hostname files under /var/lib/tor (one directory per service) usually * requires sudo: * sudo node scripts/onion-status.cjs */ "use strict"; const fs = require("fs"); const http = require("http"); const path = require("path"); const REPO = path.join(__dirname, ".."); const jsonPath = path.join(__dirname, "onion-nodes.json"); const data = JSON.parse(fs.readFileSync(jsonPath, "utf8")); /** Populated by export-onion-urls.sh — readable when /var/lib/tor is root-only. */ function readOnionFromExportFile(torDir) { const exportPath = path.join(REPO, "onion-urls.txt"); let raw; try { raw = fs.readFileSync(exportPath, "utf8"); } catch { return ""; } const line = raw.split("\n").find((l) => l.trimStart().startsWith(torDir)); if (!line) return ""; const m = line.match(/https?:\/\/([a-z2-7]{56}\.onion)\b/i); return m ? m[1] : ""; } function readOnionHost(torDir) { const f = path.join("/var/lib/tor", torDir, "hostname"); try { return fs.readFileSync(f, "utf8").trim(); } catch (e) { if (e.code === "EACCES" || e.code === "EPERM") { const fromFile = readOnionFromExportFile(torDir); if (fromFile) return `${fromFile} (from onion-urls.txt)`; return "(hostname not readable — run: sudo bash scripts/export-onion-urls.sh, then retry)"; } if (e.code === "ENOENT") { const fromFile = readOnionFromExportFile(torDir); if (fromFile) return `${fromFile} (from onion-urls.txt)`; return "(no hostname yet — is Tor running?)"; } return `(${e.message})`; } } function onionHttpUrl(onionField) { const m = String(onionField).match(/([a-z2-7]{56}\.onion)/i); if (m) return `http://${m[1]}`; return onionField; } function checkLocalPort(port) { return new Promise((resolve) => { const req = http.request( { hostname: "127.0.0.1", port, path: "/", method: "GET", timeout: 6000, headers: { Connection: "close", Host: "127.0.0.1" }, }, (res) => { res.resume(); resolve(res.statusCode || 0); }, ); req.on("error", () => resolve(0)); req.on("timeout", () => { req.destroy(); resolve(0); }); req.end(); }); } async function main() { const rows = []; for (const n of data.nodes) { const onion = readOnionHost(n.torDir); const httpCode = await checkLocalPort(n.port); const ok = httpCode >= 200 && httpCode < 500; rows.push({ torDir: n.torDir, port: n.port, kind: n.kind, onion, httpCode, ok, }); } console.log(""); console.log("CyberLux — onion URLs (from /var/lib/tor//hostname) + loopback health"); console.log("─".repeat(100)); let bad = 0; for (const r of rows) { const url = r.onion.startsWith("(") || r.onion.includes("not readable") ? r.onion : onionHttpUrl(r.onion); const local = `http://127.0.0.1:${r.port}/`; const status = r.httpCode === 0 ? "DOWN/timeout" : `HTTP ${r.httpCode}`; if (!r.ok) bad++; console.log(`${r.torDir.padEnd(28)} ${String(r.kind).padEnd(12)} ${String(r.port).padEnd(5)} ${status.padEnd(14)} ${url}`); console.log(`${"".padEnd(28)} ${"".padEnd(12)} ${"".padEnd(5)} loopback: ${local}`); console.log(""); } console.log("─".repeat(100)); if (bad > 0) { console.log(`✗ ${bad} loopback vhost(s) did not return OK HTTP — ensure: systemctl status nginx tor; systemctl status cyberlux`); process.exit(1); } console.log("✓ All loopback nginx vhosts responded (Tor .onion forwarding uses these ports)."); console.log(" To test via the Tor network use Tor Browser (not curl from clearnet)."); process.exit(0); } main().catch((e) => { console.error(e); process.exit(2); });