refactor: extract visitors.js and services/http-helpers.js from server.js
Also drops checkRateLimit/rateLimitMap -- confirmed dead code, never called by the request handler. Verified via smoke-test harness (28/28 match).
This commit is contained in:
parent
1ab6f54d10
commit
dff1bd7dd4
172
server.js
172
server.js
@ -239,27 +239,12 @@ function queryPeeringDBLocal(path) {
|
|||||||
|
|
||||||
const FEEDBACK_TOKEN = process.env.FEEDBACK_TOKEN || "changeme-set-in-env";
|
const FEEDBACK_TOKEN = process.env.FEEDBACK_TOKEN || "changeme-set-in-env";
|
||||||
const FEEDBACK_FILE = "/opt/peercortex-app/feedback.json";
|
const FEEDBACK_FILE = "/opt/peercortex-app/feedback.json";
|
||||||
const VISITORS_FILE = "/opt/peercortex-app/visitors.json";
|
|
||||||
|
|
||||||
// ── SMTP / Email ──────────────────────────────────────────────
|
// ── SMTP / Email ──────────────────────────────────────────────
|
||||||
const { sendFeedbackMail } = require('./src/backend/services/smtp');
|
const { sendFeedbackMail } = require('./src/backend/services/smtp');
|
||||||
// ── SMTP / Email ──────────────────────────────────────────────
|
// ── SMTP / Email ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
const { loadVisitors, trackVisitor } = require("./server/visitors");
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// PEERCORTEX v0.6.1 — New Features
|
// PEERCORTEX v0.6.1 — New Features
|
||||||
@ -2219,152 +2204,15 @@ async function fetchPeeringDBWithRetry(path, options) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic JSON fetch with one retry — for sources that occasionally fail under load (RIPE Stat, Atlas)
|
const {
|
||||||
async function fetchJSONWithRetry(url, options) {
|
fetchJSON,
|
||||||
const result = await fetchJSON(url, options);
|
fetchJSONWithRetry,
|
||||||
if (result !== null) return result;
|
fetchHTML,
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
postJSON,
|
||||||
return fetchJSON(url, options);
|
fetchBgproutesVisibility,
|
||||||
}
|
} = require("./server/services/http-helpers");
|
||||||
|
// checkRateLimit/rateLimitMap removed 2026-07-16: dead code, never called by
|
||||||
// bgproutes.io visibility fallback helper
|
// the request handler (confirmed via the server.js structural exploration).
|
||||||
// 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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveASNames(providers) {
|
async function resolveASNames(providers) {
|
||||||
// Batch resolve AS names via RIPE Stat AS overview API
|
// Batch resolve AS names via RIPE Stat AS overview API
|
||||||
|
|||||||
131
server/services/http-helpers.js
Normal file
131
server/services/http-helpers.js
Normal file
@ -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 };
|
||||||
21
server/visitors.js
Normal file
21
server/visitors.js
Normal file
@ -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 };
|
||||||
Loading…
x
Reference in New Issue
Block a user