const xss = require('xss'); const rateLimit = require('express-rate-limit'); const fs = require('fs'); const path = require('path'); const CONFIG_PATH = path.join(__dirname, '..', '..', 'data', 'config.json'); function readApiKey() { try { if (fs.existsSync(CONFIG_PATH)) { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')).api_key || null; } } catch {} return null; } let cachedApiKey = null; function getApiKey() { if (cachedApiKey === null) cachedApiKey = readApiKey(); return cachedApiKey; } function clearApiKeyCache() { cachedApiKey = null; } const xssOptions = { whiteList: {}, stripIgnoreTag: true, stripIgnoreTagBody: ['script', 'style', 'xml', 'iframe', 'object', 'embed'], }; function sanitize(value) { if (typeof value !== 'string') return value; return xss(value, xssOptions).trim(); } function sanitizeBody(req, res, next) { function walk(obj) { if (!obj || typeof obj !== 'object') return; for (const key of Object.keys(obj)) { if (typeof obj[key] === 'string') obj[key] = sanitize(obj[key]); else if (typeof obj[key] === 'object') walk(obj[key]); } } walk(req.body); next(); } function validateLengths(rules) { return (req, res, next) => { for (const [field, maxLen] of Object.entries(rules)) { const val = req.body[field]; if (val && typeof val === 'string' && val.length > maxLen) { return res.status(400).json({ error: `${field} 长度不能超过 ${maxLen} 个字符` }); } } next(); }; } const loginLimiter = rateLimit({ windowMs: 60 * 1000, max: 8, message: { error: '登录尝试过于频繁,请1分钟后再试' }, standardHeaders: true, legacyHeaders: false, keyGenerator: (req) => req.ip, }); const registerLimiter = rateLimit({ windowMs: 60 * 1000, max: 3, message: { error: '注册请求过于频繁,请1分钟后再试' }, standardHeaders: true, legacyHeaders: false, }); const ticketLimiter = rateLimit({ windowMs: 60 * 1000, max: 500, skip: (req) => !!(req.headers.authorization), message: { error: '提交过于频繁,请稍后再试' }, standardHeaders: true, legacyHeaders: false, }); const ticketAnonLimiter = rateLimit({ windowMs: 60 * 1000, max: 5, message: { error: '匿名提交过于频繁,请登录后再试或稍后重试' }, standardHeaders: true, legacyHeaders: false, }); const uploadLimiter = rateLimit({ windowMs: 60 * 1000, max: 5, message: { error: '上传请求过于频繁' }, standardHeaders: true, legacyHeaders: false, }); const generalLimiter = rateLimit({ windowMs: 60 * 1000, max: 300, message: { error: '请求过于频繁' }, standardHeaders: true, legacyHeaders: false, }); const captchaLimiter = rateLimit({ windowMs: 60 * 1000, max: 30, message: { error: '验证码请求过于频繁' }, }); function apiKeyGuard(req, res, next) { const p = req.originalUrl; if (p === '/api/health' || p.startsWith('/api/install')) return next(); const key = getApiKey(); if (!key) return next(); if (!req.headers['x-api-key'] || req.headers['x-api-key'].length !== key.length) return res.status(401).json({ error: '无效的 API 密钥' }); if (!timingSafeEqual(req.headers['x-api-key'], key)) return res.status(401).json({ error: '无效的 API 密钥' }); next(); } function timingSafeEqual(a, b) { let diff = a.length ^ b.length; for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i); return diff === 0; } function methodGuard(allowed) { return (req, res, next) => { if (!allowed.includes(req.method)) { return res.status(405).json({ error: `不允许 ${req.method} 方法` }); } next(); }; } module.exports = { sanitize, sanitizeBody, validateLengths, loginLimiter, registerLimiter, ticketLimiter, ticketAnonLimiter, uploadLimiter, generalLimiter, captchaLimiter, apiKeyGuard, methodGuard, clearApiKeyCache, };