修复安全边界问题
This commit is contained in:
@@ -14,6 +14,12 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem('theme');
|
||||||
const settings = window.SITE_SETTINGS || {};
|
const settings = window.SITE_SETTINGS || {};
|
||||||
|
const sanitizeAssetUrl = (url) => {
|
||||||
|
if (!url || typeof url !== 'string') return null;
|
||||||
|
const trimmed = url.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
return /^(https?:|data:image\/|\/)/i.test(trimmed) ? trimmed : null;
|
||||||
|
};
|
||||||
const defaultTheme = settings.default_theme || 'dark';
|
const defaultTheme = settings.default_theme || 'dark';
|
||||||
let theme = savedTheme || defaultTheme;
|
let theme = savedTheme || defaultTheme;
|
||||||
|
|
||||||
@@ -30,9 +36,10 @@
|
|||||||
document.title = settings.page_name;
|
document.title = settings.page_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.favicon_url) {
|
const safeFaviconUrl = sanitizeAssetUrl(settings.favicon_url);
|
||||||
|
if (safeFaviconUrl) {
|
||||||
const link = document.getElementById('siteFavicon');
|
const link = document.getElementById('siteFavicon');
|
||||||
if (link) link.href = settings.favicon_url;
|
if (link) link.href = safeFaviconUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advanced Anti-Flicker: Wait for header elements to appear
|
// Advanced Anti-Flicker: Wait for header elements to appear
|
||||||
@@ -51,9 +58,13 @@
|
|||||||
|
|
||||||
if (logoIcon) {
|
if (logoIcon) {
|
||||||
const actualTheme = document.documentElement.classList.contains('light-theme') ? 'light' : 'dark';
|
const actualTheme = document.documentElement.classList.contains('light-theme') ? 'light' : 'dark';
|
||||||
const logoToUse = (actualTheme === 'dark' && settings.logo_url_dark) ? settings.logo_url_dark : (settings.logo_url || null);
|
const logoToUse = sanitizeAssetUrl((actualTheme === 'dark' && settings.logo_url_dark) ? settings.logo_url_dark : (settings.logo_url || null));
|
||||||
if (logoToUse) {
|
if (logoToUse) {
|
||||||
logoIcon.innerHTML = '<img src="' + logoToUse + '" alt="Logo" class="logo-icon-img">';
|
const img = document.createElement('img');
|
||||||
|
img.src = logoToUse;
|
||||||
|
img.alt = 'Logo';
|
||||||
|
img.className = 'logo-icon-img';
|
||||||
|
logoIcon.replaceChildren(img);
|
||||||
} else {
|
} else {
|
||||||
// Only if we REALLY have no logo URL, we show the default SVG fallback
|
// Only if we REALLY have no logo URL, we show the default SVG fallback
|
||||||
// (But since it's already in HTML, we just don't touch it or we show it if we hid it)
|
// (But since it's already in HTML, we just don't touch it or we show it if we hid it)
|
||||||
|
|||||||
@@ -623,10 +623,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateFavicon(url) {
|
function updateFavicon(url) {
|
||||||
if (!url) return;
|
const safeUrl = sanitizeAssetUrl(url);
|
||||||
|
if (!safeUrl) return;
|
||||||
const link = dom.siteFavicon || document.querySelector("link[rel*='icon']");
|
const link = dom.siteFavicon || document.querySelector("link[rel*='icon']");
|
||||||
if (link) {
|
if (link) {
|
||||||
link.href = url;
|
link.href = safeUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,6 +676,51 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function promptLogin(message = '该操作需要登录') {
|
||||||
|
openLoginModal();
|
||||||
|
if (message) {
|
||||||
|
dom.loginError.textContent = message;
|
||||||
|
dom.loginError.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeAssetUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') return null;
|
||||||
|
const trimmed = url.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
if (/^(https?:|data:image\/|\/)/i.test(trimmed)) return trimmed;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLogoImage(url) {
|
||||||
|
if (!dom.logoIconContainer) return;
|
||||||
|
const safeUrl = sanitizeAssetUrl(url);
|
||||||
|
|
||||||
|
if (safeUrl) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = safeUrl;
|
||||||
|
img.alt = 'Logo';
|
||||||
|
img.className = 'logo-icon-img';
|
||||||
|
dom.logoIconContainer.replaceChildren(img);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.logoIconContainer.innerHTML = `
|
||||||
|
<svg class="logo-icon" id="logoSvg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="8" stroke="url(#logoGrad)" stroke-width="2.5"/>
|
||||||
|
<path d="M8 22 L12 14 L16 18 L20 10 L24 16" stroke="url(#logoGrad)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
<circle cx="12" cy="14" r="2" fill="url(#logoGrad)"/>
|
||||||
|
<circle cx="20" cy="10" r="2" fill="url(#logoGrad)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="logoGrad" x1="0" y1="0" x2="32" y2="32">
|
||||||
|
<stop offset="0%" stop-color="#6366f1"/>
|
||||||
|
<stop offset="100%" stop-color="#06b6d4"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function openLoginModal() {
|
function openLoginModal() {
|
||||||
dom.loginModal.classList.add('active');
|
dom.loginModal.classList.add('active');
|
||||||
dom.loginError.style.display = 'none';
|
dom.loginError.style.display = 'none';
|
||||||
@@ -1479,6 +1525,11 @@
|
|||||||
try {
|
try {
|
||||||
const url = `/api/metrics/server-details?instance=${encodeURIComponent(instance)}&job=${encodeURIComponent(job)}&source=${encodeURIComponent(source)}`;
|
const url = `/api/metrics/server-details?instance=${encodeURIComponent(instance)}&job=${encodeURIComponent(job)}&source=${encodeURIComponent(source)}`;
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
if (response.status === 401) {
|
||||||
|
closeServerDetail();
|
||||||
|
promptLogin('登录后可查看服务器详细指标');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!response.ok) throw new Error('Fetch failed');
|
if (!response.ok) throw new Error('Fetch failed');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
currentServerDetail.memTotal = data.memTotal;
|
currentServerDetail.memTotal = data.memTotal;
|
||||||
@@ -1697,6 +1748,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
if (res.status === 401) {
|
||||||
|
promptLogin('登录后可查看历史曲线与服务器详细信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!res.ok) throw new Error('Query failed');
|
if (!res.ok) throw new Error('Query failed');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
@@ -1883,25 +1938,7 @@
|
|||||||
logoToUse = settings.logo_url_dark;
|
logoToUse = settings.logo_url_dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logoToUse) {
|
renderLogoImage(logoToUse || null);
|
||||||
dom.logoIconContainer.innerHTML = `<img src="${escapeHtml(logoToUse)}" alt="Logo" class="logo-icon-img">`;
|
|
||||||
} else {
|
|
||||||
// Restore default SVG
|
|
||||||
dom.logoIconContainer.innerHTML = `
|
|
||||||
<svg class="logo-icon" id="logoSvg" viewBox="0 0 32 32" fill="none">
|
|
||||||
<rect x="2" y="2" width="28" height="28" rx="8" stroke="url(#logoGrad)" stroke-width="2.5"/>
|
|
||||||
<path d="M8 22 L12 14 L16 18 L20 10 L24 16" stroke="url(#logoGrad)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
||||||
<circle cx="12" cy="14" r="2" fill="url(#logoGrad)"/>
|
|
||||||
<circle cx="20" cy="10" r="2" fill="url(#logoGrad)"/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="logoGrad" x1="0" y1="0" x2="32" y2="32">
|
|
||||||
<stop offset="0%" stop-color="#6366f1"/>
|
|
||||||
<stop offset="100%" stop-color="#06b6d4"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Favicon
|
// Favicon
|
||||||
updateFavicon(settings.favicon_url);
|
updateFavicon(settings.favicon_url);
|
||||||
@@ -2021,6 +2058,10 @@
|
|||||||
async function loadLatencyRoutes() {
|
async function loadLatencyRoutes() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/latency-routes');
|
const response = await fetch('/api/latency-routes');
|
||||||
|
if (response.status === 401) {
|
||||||
|
promptLogin('登录后可管理延迟线路');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const routes = await response.json();
|
const routes = await response.json();
|
||||||
renderLatencyRoutes(routes);
|
renderLatencyRoutes(routes);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -2253,6 +2294,10 @@
|
|||||||
async function loadSources() {
|
async function loadSources() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/sources');
|
const response = await fetch('/api/sources');
|
||||||
|
if (response.status === 401) {
|
||||||
|
promptLogin('登录后可查看和管理数据源');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sources = await response.json();
|
const sources = await response.json();
|
||||||
const sourcesArray = Array.isArray(sources) ? sources : [];
|
const sourcesArray = Array.isArray(sources) ? sources : [];
|
||||||
const promSources = sourcesArray.filter(s => s.type !== 'blackbox');
|
const promSources = sourcesArray.filter(s => s.type !== 'blackbox');
|
||||||
@@ -2484,6 +2529,9 @@
|
|||||||
async function loadSourceCount() {
|
async function loadSourceCount() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/sources');
|
const response = await fetch('/api/sources');
|
||||||
|
if (response.status === 401) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sources = await response.json();
|
const sources = await response.json();
|
||||||
const sourcesArray = Array.isArray(sources) ? sources : [];
|
const sourcesArray = Array.isArray(sources) ? sources : [];
|
||||||
const promSources = sourcesArray.filter(s => s.type !== 'blackbox');
|
const promSources = sourcesArray.filter(s => s.type !== 'blackbox');
|
||||||
|
|||||||
164
server/index.js
164
server/index.js
@@ -10,7 +10,6 @@ const latencyService = require('./latency-service');
|
|||||||
const checkAndFixDatabase = require('./db-integrity-check');
|
const checkAndFixDatabase = require('./db-integrity-check');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
const dns = require('dns').promises;
|
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -24,10 +23,15 @@ const crypto = require('crypto');
|
|||||||
|
|
||||||
let isDbInitialized = false;
|
let isDbInitialized = false;
|
||||||
const sessions = new Map(); // Fallback session store when Valkey is unavailable
|
const sessions = new Map(); // Fallback session store when Valkey is unavailable
|
||||||
|
const requestBuckets = new Map();
|
||||||
const SESSION_TTL_SECONDS = parseInt(process.env.SESSION_TTL_SECONDS, 10) || 86400;
|
const SESSION_TTL_SECONDS = parseInt(process.env.SESSION_TTL_SECONDS, 10) || 86400;
|
||||||
const PASSWORD_ITERATIONS = parseInt(process.env.PASSWORD_ITERATIONS, 10) || 210000;
|
const PASSWORD_ITERATIONS = parseInt(process.env.PASSWORD_ITERATIONS, 10) || 210000;
|
||||||
const ALLOW_REMOTE_SETUP = process.env.ALLOW_REMOTE_SETUP === 'true';
|
const ALLOW_REMOTE_SETUP = process.env.ALLOW_REMOTE_SETUP === 'true';
|
||||||
const COOKIE_SECURE = process.env.COOKIE_SECURE === 'true';
|
const COOKIE_SECURE = process.env.COOKIE_SECURE === 'true';
|
||||||
|
const RATE_LIMITS = {
|
||||||
|
login: { windowMs: 15 * 60 * 1000, max: 8 },
|
||||||
|
setup: { windowMs: 10 * 60 * 1000, max: 20 }
|
||||||
|
};
|
||||||
|
|
||||||
function normalizeIp(ip) {
|
function normalizeIp(ip) {
|
||||||
if (!ip) return '';
|
if (!ip) return '';
|
||||||
@@ -60,14 +64,87 @@ function isPrivateOrLoopbackIp(ip) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isLocalRequest(req) {
|
function isLocalRequest(req) {
|
||||||
const forwardedFor = req.headers['x-forwarded-for'];
|
const ip = normalizeIp(req.socket.remoteAddress || req.ip || '');
|
||||||
const candidate = Array.isArray(forwardedFor)
|
|
||||||
? forwardedFor[0]
|
|
||||||
: (typeof forwardedFor === 'string' ? forwardedFor.split(',')[0].trim() : '');
|
|
||||||
const ip = normalizeIp(candidate || req.ip || req.socket.remoteAddress || '');
|
|
||||||
return isPrivateOrLoopbackIp(ip);
|
return isPrivateOrLoopbackIp(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getClientIp(req) {
|
||||||
|
return normalizeIp(req.socket?.remoteAddress || req.ip || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRateLimitKey(scope, req, discriminator = '') {
|
||||||
|
return `${scope}:${getClientIp(req)}:${discriminator}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkRateLimit(key, { windowMs, max }) {
|
||||||
|
const now = Date.now();
|
||||||
|
const bucket = requestBuckets.get(key);
|
||||||
|
|
||||||
|
if (!bucket || bucket.resetAt <= now) {
|
||||||
|
requestBuckets.set(key, { count: 1, resetAt: now + windowMs });
|
||||||
|
return { allowed: true, retryAfterMs: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bucket.count >= max) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
retryAfterMs: Math.max(0, bucket.resetAt - now)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket.count += 1;
|
||||||
|
return { allowed: true, retryAfterMs: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function enforceRateLimit(req, res, scope, discriminator = '') {
|
||||||
|
const result = checkRateLimit(makeRateLimitKey(scope, req, discriminator), RATE_LIMITS[scope]);
|
||||||
|
if (result.allowed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('Retry-After', Math.max(1, Math.ceil(result.retryAfterMs / 1000)));
|
||||||
|
res.status(429).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Too many requests. Please try again later.'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeJsonForInlineScript(value) {
|
||||||
|
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (char) => {
|
||||||
|
switch (char) {
|
||||||
|
case '<':
|
||||||
|
return '\\u003c';
|
||||||
|
case '>':
|
||||||
|
return '\\u003e';
|
||||||
|
case '&':
|
||||||
|
return '\\u0026';
|
||||||
|
case '\u2028':
|
||||||
|
return '\\u2028';
|
||||||
|
case '\u2029':
|
||||||
|
return '\\u2029';
|
||||||
|
default:
|
||||||
|
return char;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPublicSiteSettings(settings = {}) {
|
||||||
|
return {
|
||||||
|
page_name: settings.page_name || '',
|
||||||
|
show_page_name: settings.show_page_name !== undefined ? settings.show_page_name : 1,
|
||||||
|
title: settings.title || '',
|
||||||
|
logo_url: settings.logo_url || null,
|
||||||
|
logo_url_dark: settings.logo_url_dark || null,
|
||||||
|
favicon_url: settings.favicon_url || null,
|
||||||
|
default_theme: settings.default_theme || 'dark',
|
||||||
|
show_95_bandwidth: settings.show_95_bandwidth ? 1 : 0,
|
||||||
|
p95_type: settings.p95_type || 'tx',
|
||||||
|
icp_filing: settings.icp_filing || null,
|
||||||
|
ps_filing: settings.ps_filing || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getCookieOptions(req, maxAgeSeconds) {
|
function getCookieOptions(req, maxAgeSeconds) {
|
||||||
const options = ['Path=/', 'HttpOnly', 'SameSite=Strict'];
|
const options = ['Path=/', 'HttpOnly', 'SameSite=Strict'];
|
||||||
if (typeof maxAgeSeconds === 'number') {
|
if (typeof maxAgeSeconds === 'number') {
|
||||||
@@ -166,6 +243,9 @@ async function destroySession(sessionId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureSetupAccess(req, res, next) {
|
async function ensureSetupAccess(req, res, next) {
|
||||||
|
if (!enforceRateLimit(req, res, 'setup')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (ALLOW_REMOTE_SETUP || isLocalRequest(req)) {
|
if (ALLOW_REMOTE_SETUP || isLocalRequest(req)) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
@@ -209,11 +289,11 @@ async function checkDb() {
|
|||||||
checkDb();
|
checkDb();
|
||||||
|
|
||||||
// --- Health API ---
|
// --- Health API ---
|
||||||
app.get('/health', async (req, res) => {
|
app.get('/health', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const dbStatus = await db.checkHealth();
|
const dbStatus = await db.checkHealth();
|
||||||
const cacheStatus = await cache.checkHealth();
|
const cacheStatus = await cache.checkHealth();
|
||||||
const isAllOk = dbStatus.status === 'up' && cacheStatus.status === 'up';
|
const isAllOk = dbStatus.status === 'up' && cacheStatus.status === 'up';
|
||||||
|
|
||||||
const healthInfo = {
|
const healthInfo = {
|
||||||
status: isAllOk ? 'ok' : 'error',
|
status: isAllOk ? 'ok' : 'error',
|
||||||
@@ -228,18 +308,18 @@ app.get('/health', async (req, res) => {
|
|||||||
node_version: process.version
|
node_version: process.version
|
||||||
},
|
},
|
||||||
checks: {
|
checks: {
|
||||||
database: {
|
database: {
|
||||||
name: 'MySQL',
|
name: 'MySQL',
|
||||||
status: dbStatus.status,
|
status: dbStatus.status,
|
||||||
message: dbStatus.error || 'Connected'
|
message: dbStatus.status === 'up' ? 'Connected' : 'Unavailable'
|
||||||
},
|
},
|
||||||
valkey: {
|
valkey: {
|
||||||
name: 'Valkey (Redis)',
|
name: 'Valkey (Redis)',
|
||||||
status: cacheStatus.status,
|
status: cacheStatus.status,
|
||||||
message: cacheStatus.error || 'Connected'
|
message: cacheStatus.status === 'up' ? 'Connected' : 'Unavailable'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isAllOk) {
|
if (isAllOk) {
|
||||||
res.json(healthInfo);
|
res.json(healthInfo);
|
||||||
@@ -252,9 +332,13 @@ app.get('/health', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Auth API ---
|
// --- Auth API ---
|
||||||
app.post('/api/auth/login', async (req, res) => {
|
app.post('/api/auth/login', async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
try {
|
const loginKey = String(username || '').trim().toLowerCase();
|
||||||
|
if (!enforceRateLimit(req, res, 'login', loginKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
const [rows] = await db.query('SELECT * FROM users WHERE username = ?', [username]);
|
const [rows] = await db.query('SELECT * FROM users WHERE username = ?', [username]);
|
||||||
if (rows.length === 0) return res.status(401).json({ error: 'Invalid credentials' });
|
if (rows.length === 0) return res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
|
||||||
@@ -515,7 +599,7 @@ ENABLE_EXTERNAL_GEO_LOOKUP=${process.env.ENABLE_EXTERNAL_GEO_LOOKUP || 'false'}
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Setup Status Check
|
// Setup Status Check
|
||||||
app.get('/api/setup/status', async (req, res) => {
|
app.get('/api/setup/status', ensureSetupAccess, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!isDbInitialized) {
|
if (!isDbInitialized) {
|
||||||
return res.json({ initialized: false, step: 'db' });
|
return res.json({ initialized: false, step: 'db' });
|
||||||
@@ -628,8 +712,8 @@ const serveIndex = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inject settings
|
// Inject settings
|
||||||
const settingsJson = JSON.stringify(settings);
|
const settingsJson = escapeJsonForInlineScript(getPublicSiteSettings(settings));
|
||||||
const injection = `<script>window.SITE_SETTINGS = ${settingsJson};</script>`;
|
const injection = `<script>window.SITE_SETTINGS = ${settingsJson};</script>`;
|
||||||
|
|
||||||
// Replace <head> with <head> + injection
|
// Replace <head> with <head> + injection
|
||||||
html = html.replace('<head>', '<head>' + injection);
|
html = html.replace('<head>', '<head>' + injection);
|
||||||
@@ -649,7 +733,7 @@ app.use(express.static(path.join(__dirname, '..', 'public'), { index: false }));
|
|||||||
// ==================== Prometheus Source CRUD ====================
|
// ==================== Prometheus Source CRUD ====================
|
||||||
|
|
||||||
// Get all Prometheus sources
|
// Get all Prometheus sources
|
||||||
app.get('/api/sources', async (req, res) => {
|
app.get('/api/sources', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await db.query('SELECT * FROM prometheus_sources ORDER BY is_server_source DESC, created_at DESC');
|
const [rows] = await db.query('SELECT * FROM prometheus_sources ORDER BY is_server_source DESC, created_at DESC');
|
||||||
// Test connectivity for each source
|
// Test connectivity for each source
|
||||||
@@ -754,11 +838,15 @@ app.post('/api/sources/test', requireAuth, async (req, res) => {
|
|||||||
// ==================== Site Settings ====================
|
// ==================== Site Settings ====================
|
||||||
|
|
||||||
// Get site settings
|
// Get site settings
|
||||||
app.get('/api/settings', async (req, res) => {
|
app.get('/api/settings', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await db.query('SELECT * FROM site_settings WHERE id = 1');
|
const [rows] = await db.query('SELECT * FROM site_settings WHERE id = 1');
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return res.json({
|
return res.json(getPublicSiteSettings());
|
||||||
|
}
|
||||||
|
return res.json(getPublicSiteSettings(rows[0]));
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return res.json({
|
||||||
page_name: '数据可视化展示大屏',
|
page_name: '数据可视化展示大屏',
|
||||||
show_page_name: 1,
|
show_page_name: 1,
|
||||||
title: '数据可视化展示大屏',
|
title: '数据可视化展示大屏',
|
||||||
@@ -1065,7 +1153,7 @@ app.get('/api/metrics/cpu-history', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get detailed metrics for a specific server
|
// Get detailed metrics for a specific server
|
||||||
app.get('/api/metrics/server-details', async (req, res) => {
|
app.get('/api/metrics/server-details', requireAuth, async (req, res) => {
|
||||||
const { instance, job, source } = req.query;
|
const { instance, job, source } = req.query;
|
||||||
|
|
||||||
if (!instance || !job || !source) {
|
if (!instance || !job || !source) {
|
||||||
@@ -1090,7 +1178,7 @@ app.get('/api/metrics/server-details', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get historical metrics for a specific server
|
// Get historical metrics for a specific server
|
||||||
app.get('/api/metrics/server-history', async (req, res) => {
|
app.get('/api/metrics/server-history', requireAuth, async (req, res) => {
|
||||||
const { instance, job, source, metric, range, start, end } = req.query;
|
const { instance, job, source, metric, range, start, end } = req.query;
|
||||||
|
|
||||||
if (!instance || !job || !source || !metric) {
|
if (!instance || !job || !source || !metric) {
|
||||||
@@ -1123,7 +1211,7 @@ app.get('*', (req, res, next) => {
|
|||||||
|
|
||||||
|
|
||||||
// ==================== Latency Routes CRUD ====================
|
// ==================== Latency Routes CRUD ====================
|
||||||
|
|
||||||
app.get('/api/latency-routes', requireAuth, async (req, res) => {
|
app.get('/api/latency-routes', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await db.query(`
|
const [rows] = await db.query(`
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ const QUERY_TIMEOUT = 10000;
|
|||||||
// Reusable agents to handle potential redirect issues and protocol mismatches
|
// Reusable agents to handle potential redirect issues and protocol mismatches
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const httpAgent = new http.Agent({ keepAlive: true });
|
const httpAgent = new http.Agent({ keepAlive: true });
|
||||||
const httpsAgent = new https.Agent({ keepAlive: true, rejectUnauthorized: false });
|
const httpsAgent = new https.Agent({ keepAlive: true });
|
||||||
|
|
||||||
const serverIdMap = new Map(); // token -> { instance, job, source }
|
const serverIdMap = new Map(); // token -> { instance, job, source }
|
||||||
const SECRET = process.env.APP_SECRET || 'prom-data-panel-stable-secret-key-123';
|
const SECRET = process.env.APP_SECRET || crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
function getServerToken(instance, job, source) {
|
function getServerToken(instance, job, source) {
|
||||||
const hash = crypto.createHmac('sha256', SECRET)
|
const hash = crypto.createHmac('sha256', SECRET)
|
||||||
|
|||||||
Reference in New Issue
Block a user