diff --git a/server.js b/server.js index 224033f..ce8c709 100644 --- a/server.js +++ b/server.js @@ -239,27 +239,12 @@ function queryPeeringDBLocal(path) { const FEEDBACK_TOKEN = process.env.FEEDBACK_TOKEN || "changeme-set-in-env"; const FEEDBACK_FILE = "/opt/peercortex-app/feedback.json"; -const VISITORS_FILE = "/opt/peercortex-app/visitors.json"; // ── SMTP / Email ────────────────────────────────────────────── const { sendFeedbackMail } = require('./src/backend/services/smtp'); // ── SMTP / Email ────────────────────────────────────────────── - -function loadVisitors() { - try { return JSON.parse(fs.readFileSync(VISITORS_FILE, "utf8")); } catch (_) { return { hashes: [] }; } -} - -function trackVisitor(req) { - const ip = (req.headers["x-forwarded-for"] || "").split(",")[0].trim() || (req.socket && req.socket.remoteAddress) || ""; - const hash = crypto.createHash("sha256").update(ip + "peercortex-salt-2026").digest("hex"); - const data = loadVisitors(); - if (!data.hashes.includes(hash)) { - data.hashes.push(hash); - try { fs.writeFileSync(VISITORS_FILE, JSON.stringify(data)); } catch (_) {} - } - return data.hashes.length; -} +const { loadVisitors, trackVisitor } = require("./server/visitors"); // ═══════════════════════════════════════════════════════════════ // PEERCORTEX v0.6.1 — New Features @@ -2219,152 +2204,15 @@ async function fetchPeeringDBWithRetry(path, options) { return result; } -// Generic JSON fetch with one retry — for sources that occasionally fail under load (RIPE Stat, Atlas) -async function fetchJSONWithRetry(url, options) { - const result = await fetchJSON(url, options); - if (result !== null) return result; - await new Promise(r => setTimeout(r, 1000)); - return fetchJSON(url, options); -} - -// bgproutes.io visibility fallback helper -// Queries the RIB endpoint to estimate prefix visibility across vantage points -function fetchBgproutesVisibility(prefix) { - if (!BGPROUTES_API_KEY) return Promise.resolve(null); - const url = BGPROUTES_API_URL + "/rib?prefix=" + encodeURIComponent(prefix) + "&prefix_match=exact"; - return fetchJSON(url, { - timeout: 15000, - headers: { - "Authorization": "Bearer " + BGPROUTES_API_KEY, - "User-Agent": UA, - }, - }).then(function(data) { - if (!data || !data.data) return null; - // data.data should be an array of RIB entries from different vantage points - var entries = Array.isArray(data.data) ? data.data : (data.data.entries || data.data.routes || []); - var vpSet = new Set(); - entries.forEach(function(e) { - if (e.vantage_point || e.vp || e.collector || e.peer_asn) { - vpSet.add(e.vantage_point || e.vp || e.collector || e.peer_asn); - } - }); - return { vps_seeing: vpSet.size, total_entries: entries.length, source: "bgproutes.io" }; - }).catch(function() { return null; }); -} - -// Rate limiting: max 60 requests per minute per IP -const rateLimitMap = new Map(); -const RATE_LIMIT_WINDOW = 60 * 1000; -const RATE_LIMIT_MAX = 60; - -function checkRateLimit(ip) { - const now = Date.now(); - let entry = rateLimitMap.get(ip); - if (!entry || now > entry.windowStart + RATE_LIMIT_WINDOW) { - entry = { windowStart: now, count: 0 }; - rateLimitMap.set(ip, entry); - } - entry.count++; - // Clean old entries periodically - if (rateLimitMap.size > 1000) { - for (const [k, v] of rateLimitMap) { - if (now > v.windowStart + RATE_LIMIT_WINDOW) rateLimitMap.delete(k); - } - } - return entry.count <= RATE_LIMIT_MAX; -} - -function fetchJSON(url, options) { - const timeoutMs = (options && options.timeout) || 8000; - return new Promise((resolve) => { - const reqOptions = { - headers: { "User-Agent": UA, ...(options && options.headers ? options.headers : {}) }, - timeout: timeoutMs, - }; - const timer = setTimeout(() => resolve(null), timeoutMs + 500); - https - .get(url, reqOptions, (res) => { - let data = ""; - res.on("data", (chunk) => (data += chunk)); - res.on("end", () => { - clearTimeout(timer); - if (res.statusCode === 429) { - console.warn("[PDB] Rate limited (429):", url.substring(0, 80)); - return resolve(null); - } - try { - resolve(JSON.parse(data)); - } catch (_e) { - resolve(null); - } - }); - }) - .on("timeout", () => { clearTimeout(timer); resolve(null); }) - .on("error", () => { clearTimeout(timer); resolve(null); }); - }); -} - - -function fetchHTML(url, options) { - return new Promise((resolve) => { - const reqOptions = { - headers: { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - ...(options && options.headers ? options.headers : {}), - }, - }; - const lib = url.startsWith("https") ? require("https") : require("http"); - lib - .get(url, reqOptions, (res) => { - if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - return fetchHTML(res.headers.location, options).then(resolve); - } - let data = ""; - res.on("data", (chunk) => (data += chunk)); - res.on("end", () => resolve(data)); - }) - .on("error", () => resolve(null)); - }); -} - -function postJSON(url, body, options) { - return new Promise((resolve) => { - const data = JSON.stringify(body); - const parsed = new URL(url); - const timeout = (options && options.timeout) || 10000; - const reqOptions = { - hostname: parsed.hostname, - port: parsed.port || 443, - path: parsed.pathname + parsed.search, - method: "POST", - headers: { - "User-Agent": UA, - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(data), - ...(options && options.headers ? options.headers : {}), - }, - }; - let done = false; - const timer = setTimeout(() => { if (!done) { done = true; req.destroy(); resolve(null); } }, timeout); - const req = https.request(reqOptions, (res) => { - let chunks = ""; - res.on("data", (chunk) => (chunks += chunk)); - res.on("end", () => { - if (done) return; - done = true; - clearTimeout(timer); - try { - resolve(JSON.parse(chunks)); - } catch (_e) { - resolve(null); - } - }); - }); - req.on("error", () => { if (!done) { done = true; clearTimeout(timer); resolve(null); } }); - req.write(data); - req.end(); - }); -} +const { + fetchJSON, + fetchJSONWithRetry, + fetchHTML, + postJSON, + fetchBgproutesVisibility, +} = require("./server/services/http-helpers"); +// checkRateLimit/rateLimitMap removed 2026-07-16: dead code, never called by +// the request handler (confirmed via the server.js structural exploration). async function resolveASNames(providers) { // Batch resolve AS names via RIPE Stat AS overview API diff --git a/server/services/http-helpers.js b/server/services/http-helpers.js new file mode 100644 index 0000000..a4e6809 --- /dev/null +++ b/server/services/http-helpers.js @@ -0,0 +1,131 @@ +const https = require("https"); +const { UA } = require("../data/constants"); + +const BGPROUTES_API_KEY = process.env.BGPROUTES_API_KEY || ""; +const BGPROUTES_API_URL = process.env.BGPROUTES_API_URL || "https://api.bgproutes.io/v1"; + +function fetchJSON(url, options) { + const timeoutMs = (options && options.timeout) || 8000; + return new Promise((resolve) => { + const reqOptions = { + headers: { "User-Agent": UA, ...(options && options.headers ? options.headers : {}) }, + timeout: timeoutMs, + }; + const timer = setTimeout(() => resolve(null), timeoutMs + 500); + https + .get(url, reqOptions, (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + clearTimeout(timer); + if (res.statusCode === 429) { + console.warn("[PDB] Rate limited (429):", url.substring(0, 80)); + return resolve(null); + } + try { + resolve(JSON.parse(data)); + } catch (_e) { + resolve(null); + } + }); + }) + .on("timeout", () => { clearTimeout(timer); resolve(null); }) + .on("error", () => { clearTimeout(timer); resolve(null); }); + }); +} + +// Generic JSON fetch with one retry — for sources that occasionally fail under load (RIPE Stat, Atlas) +async function fetchJSONWithRetry(url, options) { + const result = await fetchJSON(url, options); + if (result !== null) return result; + await new Promise(r => setTimeout(r, 1000)); + return fetchJSON(url, options); +} + +function fetchHTML(url, options) { + return new Promise((resolve) => { + const reqOptions = { + headers: { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + ...(options && options.headers ? options.headers : {}), + }, + }; + const lib = url.startsWith("https") ? require("https") : require("http"); + lib + .get(url, reqOptions, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + return fetchHTML(res.headers.location, options).then(resolve); + } + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => resolve(data)); + }) + .on("error", () => resolve(null)); + }); +} + +function postJSON(url, body, options) { + return new Promise((resolve) => { + const data = JSON.stringify(body); + const parsed = new URL(url); + const timeout = (options && options.timeout) || 10000; + const reqOptions = { + hostname: parsed.hostname, + port: parsed.port || 443, + path: parsed.pathname + parsed.search, + method: "POST", + headers: { + "User-Agent": UA, + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(data), + ...(options && options.headers ? options.headers : {}), + }, + }; + let done = false; + const timer = setTimeout(() => { if (!done) { done = true; req.destroy(); resolve(null); } }, timeout); + const req = https.request(reqOptions, (res) => { + let chunks = ""; + res.on("data", (chunk) => (chunks += chunk)); + res.on("end", () => { + if (done) return; + done = true; + clearTimeout(timer); + try { + resolve(JSON.parse(chunks)); + } catch (_e) { + resolve(null); + } + }); + }); + req.on("error", () => { if (!done) { done = true; clearTimeout(timer); resolve(null); } }); + req.write(data); + req.end(); + }); +} + +// bgproutes.io visibility fallback helper +// Queries the RIB endpoint to estimate prefix visibility across vantage points +function fetchBgproutesVisibility(prefix) { + if (!BGPROUTES_API_KEY) return Promise.resolve(null); + const url = BGPROUTES_API_URL + "/rib?prefix=" + encodeURIComponent(prefix) + "&prefix_match=exact"; + return fetchJSON(url, { + timeout: 15000, + headers: { + "Authorization": "Bearer " + BGPROUTES_API_KEY, + "User-Agent": UA, + }, + }).then(function(data) { + if (!data || !data.data) return null; + // data.data should be an array of RIB entries from different vantage points + var entries = Array.isArray(data.data) ? data.data : (data.data.entries || data.data.routes || []); + var vpSet = new Set(); + entries.forEach(function(e) { + if (e.vantage_point || e.vp || e.collector || e.peer_asn) { + vpSet.add(e.vantage_point || e.vp || e.collector || e.peer_asn); + } + }); + return { vps_seeing: vpSet.size, total_entries: entries.length, source: "bgproutes.io" }; + }).catch(function() { return null; }); +} + +module.exports = { fetchJSON, fetchJSONWithRetry, fetchHTML, postJSON, fetchBgproutesVisibility }; diff --git a/server/visitors.js b/server/visitors.js new file mode 100644 index 0000000..b1ad486 --- /dev/null +++ b/server/visitors.js @@ -0,0 +1,21 @@ +const fs = require("fs"); +const crypto = require("crypto"); + +const VISITORS_FILE = "/opt/peercortex-app/visitors.json"; + +function loadVisitors() { + try { return JSON.parse(fs.readFileSync(VISITORS_FILE, "utf8")); } catch (_) { return { hashes: [] }; } +} + +function trackVisitor(req) { + const ip = (req.headers["x-forwarded-for"] || "").split(",")[0].trim() || (req.socket && req.socket.remoteAddress) || ""; + const hash = crypto.createHash("sha256").update(ip + "peercortex-salt-2026").digest("hex"); + const data = loadVisitors(); + if (!data.hashes.includes(hash)) { + data.hashes.push(hash); + try { fs.writeFileSync(VISITORS_FILE, JSON.stringify(data)); } catch (_) {} + } + return data.hashes.length; +} + +module.exports = { loadVisitors, trackVisitor };