From 3399392f1fdbdb0a57a1c1ca59c1c056e465cb8d Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Fri, 17 Jul 2026 07:31:19 +0200 Subject: [PATCH] refactor: extract static/search/feedback route handlers server/routes/static.js (/, /index.html, /index-editorial.html, /favicon.ico, /lia), server/routes/search.js (/api/search, /api/visitors), server/routes/feedback.js (/api/feedback OPTIONS/POST/GET). Verified via node --check, eslint no-undef (only the 3 known pre-existing bugs remain, no new ones), and smoke-test harness (28/28 match). server.js: 3714 -> 3556 lines. --- server.js | 199 ++++---------------------------------- server/routes/feedback.js | 70 ++++++++++++++ server/routes/search.js | 83 ++++++++++++++++ server/routes/static.js | 61 ++++++++++++ 4 files changed, 234 insertions(+), 179 deletions(-) create mode 100644 server/routes/feedback.js create mode 100644 server/routes/search.js create mode 100644 server/routes/static.js diff --git a/server.js b/server.js index 2a7f4e5..7b66ddb 100644 --- a/server.js +++ b/server.js @@ -36,9 +36,6 @@ const { fetchPeeringDBWithRetry, } = require("./server/services/peeringdb"); -const FEEDBACK_TOKEN = process.env.FEEDBACK_TOKEN || "changeme-set-in-env"; -const FEEDBACK_FILE = "/opt/peercortex-app/feedback.json"; - // ── SMTP / Email ────────────────────────────────────────────── const { sendFeedbackMail } = require('./src/backend/services/smtp'); // ── SMTP / Email ────────────────────────────────────────────── @@ -220,6 +217,10 @@ function proxyToApiServer(req, res, targetUrl) { // HTTP Server // ============================================================ +const staticRoutes = require("./server/routes/static"); +const searchRoutes = require("./server/routes/search"); +const feedbackRoutes = require("./server/routes/feedback"); + const server = http.createServer(async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); @@ -243,211 +244,51 @@ const server = http.createServer(async (req, res) => { const host = (req.headers.host || '').split(':')[0]; if (reqPath === "/" || reqPath === "/index.html") { - // shell.peercortex.org → admin feedback terminal (check first) - if (host === 'shell.peercortex.org') { - try { - const html = fs.readFileSync('/opt/peercortex-app/public/shell.html', 'utf8'); - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - return res.end(html); - } catch (_e) { - res.writeHead(500); - return res.end('shell.html not found'); - } - } - // v2.peercortex.org → redirect to main domain - if (host === 'v2.peercortex.org') { - res.writeHead(301, { Location: 'https://peercortex.org' + reqPath }); - return res.end(); - } - const htmlFile = "index.html"; - try { - const html = fs.readFileSync("/opt/peercortex-app/public/" + htmlFile, "utf8"); - res.setHeader("Content-Type", "text/html; charset=utf-8"); - return res.end(html); - } catch (_e) { - res.writeHead(500); - return res.end(htmlFile + " not found"); - } + return staticRoutes.handleRoot(req, res, host, reqPath); } // Direct access to editorial version if (reqPath === "/index-editorial.html") { - try { - const html = fs.readFileSync("/opt/peercortex-app/public/index-editorial.html", "utf8"); - res.setHeader("Content-Type", "text/html; charset=utf-8"); - return res.end(html); - } catch (_e) { - res.writeHead(500); - return res.end("index-editorial.html not found"); - } + return staticRoutes.handleIndexEditorial(req, res); } - // ============================================================ - // Feedback API - // ============================================================ - - - // ── Name Search (RIPE Stat + PeeringDB combined) ───────────── - if (reqPath === '/api/search') { - const params = new URL(req.url, 'http://localhost').searchParams; - const q = (params.get('q') || '').trim(); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Cache-Control', 'public, max-age=120'); - if (!q || q.length < 2) { res.writeHead(400); return res.end(JSON.stringify({error:'query too short'})); } - try { - const results = []; - const seen = new Set(); - - // Source 1: RIPE Stat searchcomplete (fast, covers ASNs + org names) - try { - const ripeUrl = 'https://stat.ripe.net/data/searchcomplete/data.json?resource=' + encodeURIComponent(q); - const ripeData = await fetchJSONWithRetry(ripeUrl, { timeout: 6000 }); - const cats = ripeData && ripeData.data && ripeData.data.categories || []; - for (var ci = 0; ci < cats.length; ci++) { - var suggs = cats[ci].suggestions || []; - for (var si = 0; si < suggs.length; si++) { - var s = suggs[si]; - var val = (s.value || '').toString(); - // Only ASN results - if (/^AS\d+$/i.test(val) && !seen.has(val)) { - seen.add(val); - // Use description (e.g. "FLEXOPTIX, DE") as the display label - var ripeName = s.description || s.label || val; - results.push({ asn: val.replace(/^AS/i,''), label: ripeName, description: '', source: 'RIPE Stat' }); - } - } - } - } catch(e) { /* RIPE Stat failed, continue */ } - - // Source 2: PeeringDB name search (best for network operator names) - try { - var pdbUrl = 'https://www.peeringdb.com/api/net?name__icontains=' + encodeURIComponent(q) + '&depth=1&limit=10'; - if (PEERINGDB_API_KEY) pdbUrl += '&key=' + PEERINGDB_API_KEY; - const pdbData = await fetchJSONWithRetry(pdbUrl, { timeout: 8000 }); - var nets = pdbData && pdbData.data || []; - for (var ni = 0; ni < nets.length; ni++) { - var net = nets[ni]; - var asnKey = 'AS' + net.asn; - if (net.asn && !seen.has(asnKey)) { - seen.add(asnKey); - var pdbDesc = [net.info_type, net.country].filter(Boolean).join(' · '); - results.push({ - asn: String(net.asn), - label: net.name || asnKey, - description: pdbDesc, - source: 'PeeringDB' - }); - } - } - } catch(e) { /* PeeringDB failed, continue */ } - - // Sort: RIPE results first (usually more relevant for ASN lookup), then PeeringDB - results.sort((a, b) => { - if (a.source === b.source) return 0; - return a.source === 'RIPE Stat' ? -1 : 1; - }); - - res.writeHead(200); - return res.end(JSON.stringify({ q: q, results: results.slice(0, 12) })); - } catch(e) { - res.writeHead(500); return res.end(JSON.stringify({error: e.message})); - } + // Name Search (RIPE Stat + PeeringDB combined) + if (reqPath === "/api/search") { + return searchRoutes.handleSearch(req, res); } // GET /api/visitors — unique visitor count if (reqPath === "/api/visitors" && req.method === "GET") { - res.setHeader("Content-Type", "application/json"); - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Cache-Control", "no-store"); - const count = trackVisitor(req); - res.writeHead(200); - return res.end(JSON.stringify({ visitors: count })); + return searchRoutes.handleVisitors(req, res); } // OPTIONS preflight (CORS) - if (reqPath === '/api/feedback' && req.method === 'OPTIONS') { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - res.writeHead(204); - return res.end(); + if (reqPath === "/api/feedback" && req.method === "OPTIONS") { + return feedbackRoutes.handleOptions(req, res); } // POST /api/feedback — submit feedback entry - if (reqPath === '/api/feedback' && req.method === 'POST') { - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Access-Control-Allow-Origin', '*'); - let body = ''; - req.on('data', function(chunk) { body += chunk; }); - req.on('end', function() { - try { - const data = JSON.parse(body); - if (!data.message || String(data.message).trim().length < 3) { - res.writeHead(400); - return res.end(JSON.stringify({ ok: false, error: 'Message too short' })); - } - const entry = { - id: Date.now() + '-' + Math.random().toString(36).slice(2, 7), - timestamp: new Date().toISOString(), - category: String(data.category || 'General').slice(0, 50), - message: String(data.message || '').slice(0, 2000), - name: String(data.name || 'Anonymous').slice(0, 100), - asn: data.asn ? String(data.asn).slice(0, 20) : null, - ip: req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.socket.remoteAddress || null, - ua: String(req.headers['user-agent'] || '').slice(0, 200) - }; - let entries = []; - try { entries = JSON.parse(fs.readFileSync(FEEDBACK_FILE, 'utf8')); } catch (_e) { /* no file yet */ } - entries.push(entry); - fs.writeFileSync(FEEDBACK_FILE, JSON.stringify(entries, null, 2)); - // Send email async — don't block response - sendFeedbackMail(entry).catch(e => console.error('[MAIL] Failed:', e.message)); - return res.end(JSON.stringify({ ok: true, id: entry.id })); - } catch (_e) { - res.writeHead(500); - return res.end(JSON.stringify({ ok: false, error: 'Server error' })); - } - }); - return; + if (reqPath === "/api/feedback" && req.method === "POST") { + return feedbackRoutes.handlePost(req, res); } // GET /api/feedback?token=... — admin: fetch all entries as JSON - if (reqPath === '/api/feedback' && req.method === 'GET') { - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Access-Control-Allow-Origin', '*'); - const token = url.searchParams.get('token'); - if (!token || token !== FEEDBACK_TOKEN) { - res.writeHead(401); - return res.end(JSON.stringify({ ok: false, error: 'Unauthorized' })); - } - try { - const entries = JSON.parse(fs.readFileSync(FEEDBACK_FILE, 'utf8')); - return res.end(JSON.stringify({ ok: true, entries: entries, count: entries.length })); - } catch (_e) { - return res.end(JSON.stringify({ ok: true, entries: [], count: 0 })); - } + if (reqPath === "/api/feedback" && req.method === "GET") { + return feedbackRoutes.handleGet(req, res, url); } // Serve favicon if (reqPath === "/favicon.ico") { - res.writeHead(204); - return res.end(); + return staticRoutes.handleFavicon(req, res); } - // Lia's Atlas Paradise - Easter egg page + // Lia'''s Atlas Paradise - Easter egg page if (reqPath === "/lia" || reqPath === "/lia/") { - try { - const liaHtml = fs.readFileSync(__dirname + "/public/lia.html", "utf8"); - res.setHeader("Content-Type", "text/html; charset=utf-8"); - return res.end(liaHtml); - } catch (_e) { - res.writeHead(500); - return res.end("lia.html not found"); - } + return staticRoutes.handleLia(req, res); } + // ============================================================ // Lia's Atlas Paradise: Atlas probe coverage endpoint // ============================================================ diff --git a/server/routes/feedback.js b/server/routes/feedback.js new file mode 100644 index 0000000..630df32 --- /dev/null +++ b/server/routes/feedback.js @@ -0,0 +1,70 @@ +const fs = require("fs"); +const { sendFeedbackMail } = require("../../src/backend/services/smtp"); + +const FEEDBACK_TOKEN = process.env.FEEDBACK_TOKEN || "changeme-set-in-env"; +const FEEDBACK_FILE = "/opt/peercortex-app/feedback.json"; + +function handleOptions(req, res) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + res.writeHead(204); + return res.end(); +} + +// POST /api/feedback — submit feedback entry +function handlePost(req, res) { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Access-Control-Allow-Origin', '*'); + let body = ''; + req.on('data', function(chunk) { body += chunk; }); + req.on('end', function() { + try { + const data = JSON.parse(body); + if (!data.message || String(data.message).trim().length < 3) { + res.writeHead(400); + return res.end(JSON.stringify({ ok: false, error: 'Message too short' })); + } + const entry = { + id: Date.now() + '-' + Math.random().toString(36).slice(2, 7), + timestamp: new Date().toISOString(), + category: String(data.category || 'General').slice(0, 50), + message: String(data.message || '').slice(0, 2000), + name: String(data.name || 'Anonymous').slice(0, 100), + asn: data.asn ? String(data.asn).slice(0, 20) : null, + ip: req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.socket.remoteAddress || null, + ua: String(req.headers['user-agent'] || '').slice(0, 200) + }; + let entries = []; + try { entries = JSON.parse(fs.readFileSync(FEEDBACK_FILE, 'utf8')); } catch (_e) { /* no file yet */ } + entries.push(entry); + fs.writeFileSync(FEEDBACK_FILE, JSON.stringify(entries, null, 2)); + // Send email async — don't block response + sendFeedbackMail(entry).catch(e => console.error('[MAIL] Failed:', e.message)); + return res.end(JSON.stringify({ ok: true, id: entry.id })); + } catch (_e) { + res.writeHead(500); + return res.end(JSON.stringify({ ok: false, error: 'Server error' })); + } + }); + return; +} + +// GET /api/feedback?token=... — admin: fetch all entries as JSON +function handleGet(req, res, url) { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Access-Control-Allow-Origin', '*'); + const token = url.searchParams.get('token'); + if (!token || token !== FEEDBACK_TOKEN) { + res.writeHead(401); + return res.end(JSON.stringify({ ok: false, error: 'Unauthorized' })); + } + try { + const entries = JSON.parse(fs.readFileSync(FEEDBACK_FILE, 'utf8')); + return res.end(JSON.stringify({ ok: true, entries: entries, count: entries.length })); + } catch (_e) { + return res.end(JSON.stringify({ ok: true, entries: [], count: 0 })); + } +} + +module.exports = { handleOptions, handlePost, handleGet }; diff --git a/server/routes/search.js b/server/routes/search.js new file mode 100644 index 0000000..4372ead --- /dev/null +++ b/server/routes/search.js @@ -0,0 +1,83 @@ +const { fetchJSONWithRetry } = require("../services/http-helpers"); +const { PEERINGDB_API_KEY } = require("../services/peeringdb"); +const { trackVisitor } = require("../visitors"); + +// Name Search (RIPE Stat + PeeringDB combined) +async function handleSearch(req, res) { + const params = new URL(req.url, 'http://localhost').searchParams; + const q = (params.get('q') || '').trim(); + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Cache-Control', 'public, max-age=120'); + if (!q || q.length < 2) { res.writeHead(400); return res.end(JSON.stringify({error:'query too short'})); } + try { + const results = []; + const seen = new Set(); + + // Source 1: RIPE Stat searchcomplete (fast, covers ASNs + org names) + try { + const ripeUrl = 'https://stat.ripe.net/data/searchcomplete/data.json?resource=' + encodeURIComponent(q); + const ripeData = await fetchJSONWithRetry(ripeUrl, { timeout: 6000 }); + const cats = ripeData && ripeData.data && ripeData.data.categories || []; + for (var ci = 0; ci < cats.length; ci++) { + var suggs = cats[ci].suggestions || []; + for (var si = 0; si < suggs.length; si++) { + var s = suggs[si]; + var val = (s.value || '').toString(); + // Only ASN results + if (/^AS\d+$/i.test(val) && !seen.has(val)) { + seen.add(val); + // Use description (e.g. "FLEXOPTIX, DE") as the display label + var ripeName = s.description || s.label || val; + results.push({ asn: val.replace(/^AS/i,''), label: ripeName, description: '', source: 'RIPE Stat' }); + } + } + } + } catch(e) { /* RIPE Stat failed, continue */ } + + // Source 2: PeeringDB name search (best for network operator names) + try { + var pdbUrl = 'https://www.peeringdb.com/api/net?name__icontains=' + encodeURIComponent(q) + '&depth=1&limit=10'; + if (PEERINGDB_API_KEY) pdbUrl += '&key=' + PEERINGDB_API_KEY; + const pdbData = await fetchJSONWithRetry(pdbUrl, { timeout: 8000 }); + var nets = pdbData && pdbData.data || []; + for (var ni = 0; ni < nets.length; ni++) { + var net = nets[ni]; + var asnKey = 'AS' + net.asn; + if (net.asn && !seen.has(asnKey)) { + seen.add(asnKey); + var pdbDesc = [net.info_type, net.country].filter(Boolean).join(' · '); + results.push({ + asn: String(net.asn), + label: net.name || asnKey, + description: pdbDesc, + source: 'PeeringDB' + }); + } + } + } catch(e) { /* PeeringDB failed, continue */ } + + // Sort: RIPE results first (usually more relevant for ASN lookup), then PeeringDB + results.sort((a, b) => { + if (a.source === b.source) return 0; + return a.source === 'RIPE Stat' ? -1 : 1; + }); + + res.writeHead(200); + return res.end(JSON.stringify({ q: q, results: results.slice(0, 12) })); + } catch(e) { + res.writeHead(500); return res.end(JSON.stringify({error: e.message})); + } +} + +// GET /api/visitors — unique visitor count +function handleVisitors(req, res) { + res.setHeader("Content-Type", "application/json"); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Cache-Control", "no-store"); + const count = trackVisitor(req); + res.writeHead(200); + return res.end(JSON.stringify({ visitors: count })); +} + +module.exports = { handleSearch, handleVisitors }; diff --git a/server/routes/static.js b/server/routes/static.js new file mode 100644 index 0000000..7364a5e --- /dev/null +++ b/server/routes/static.js @@ -0,0 +1,61 @@ +const fs = require("fs"); +const path = require("path"); + +const REPO_ROOT = path.join(__dirname, "..", ".."); + +function handleRoot(req, res, host, reqPath) { + // shell.peercortex.org → admin feedback terminal (check first) + if (host === 'shell.peercortex.org') { + try { + const html = fs.readFileSync('/opt/peercortex-app/public/shell.html', 'utf8'); + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + return res.end(html); + } catch (_e) { + res.writeHead(500); + return res.end('shell.html not found'); + } + } + // v2.peercortex.org → redirect to main domain + if (host === 'v2.peercortex.org') { + res.writeHead(301, { Location: 'https://peercortex.org' + reqPath }); + return res.end(); + } + const htmlFile = "index.html"; + try { + const html = fs.readFileSync("/opt/peercortex-app/public/" + htmlFile, "utf8"); + res.setHeader("Content-Type", "text/html; charset=utf-8"); + return res.end(html); + } catch (_e) { + res.writeHead(500); + return res.end(htmlFile + " not found"); + } +} + +function handleIndexEditorial(req, res) { + try { + const html = fs.readFileSync("/opt/peercortex-app/public/index-editorial.html", "utf8"); + res.setHeader("Content-Type", "text/html; charset=utf-8"); + return res.end(html); + } catch (_e) { + res.writeHead(500); + return res.end("index-editorial.html not found"); + } +} + +function handleFavicon(req, res) { + res.writeHead(204); + return res.end(); +} + +function handleLia(req, res) { + try { + const liaHtml = fs.readFileSync(path.join(REPO_ROOT, "public", "lia.html"), "utf8"); + res.setHeader("Content-Type", "text/html; charset=utf-8"); + return res.end(liaHtml); + } catch (_e) { + res.writeHead(500); + return res.end("lia.html not found"); + } +} + +module.exports = { handleRoot, handleIndexEditorial, handleFavicon, handleLia };