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_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
|
||||
|
||||
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