refactor: modularize backend security and observability

This commit is contained in:
237899745
2026-07-27 13:06:54 +08:00
parent f90311e68c
commit dce1e3622a
25 changed files with 1633 additions and 849 deletions

View File

@@ -0,0 +1,113 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const projectRoot = path.resolve(__dirname, '..', '..');
const serverPath = path.join(projectRoot, 'backend', 'server.js');
const source = fs.readFileSync(serverPath, 'utf8');
const results = { passed: 0, failed: 0 };
function test(name, fn) {
try {
fn();
results.passed += 1;
console.log(` [PASS] ${name}`);
} catch (error) {
results.failed += 1;
console.error(` [FAIL] ${name}: ${error.message}`);
}
}
console.log('\n========== 架构约束测试 ==========\n');
test('Express async 错误补丁在所有路由之前加载', () => {
const patchIndex = source.indexOf("require('express-async-errors')");
const routeIndex = source.search(/\bapp\.(get|post|put|delete|patch)\s*\(/);
assert.ok(patchIndex >= 0);
assert.ok(routeIndex > patchIndex);
});
test('全局错误中间件保持为最后一个 app.use', () => {
const errorHandlerIndex = source.indexOf('app.use(expressErrorHandler);');
assert.ok(errorHandlerIndex >= 0);
assert.strictEqual(source.lastIndexOf('app.use('), errorHandlerIndex);
});
test('抽取的纯函数不在 server.js 保留第二份定义', () => {
const extractedFunctions = [
'normalizeVersion',
'normalizeSha256',
'normalizeVirtualPath',
'buildHttpDownloadUrl',
'parseDownloadTrafficLine',
'isShareIpAllowed',
'resolveClientType',
'parseBooleanLike'
];
for (const functionName of extractedFunctions) {
assert.strictEqual(new RegExp(`function\\s+${functionName}\\s*\\(`).test(source), false, functionName);
}
});
test('旧占位路由已清除且真实系统路由已挂载', () => {
assert.strictEqual(fs.existsSync(path.join(projectRoot, 'backend', 'routes', 'health.js')), false);
assert.strictEqual(fs.existsSync(path.join(projectRoot, 'backend', 'routes', 'index.js')), false);
assert.ok(source.includes("require('./routes/system.routes')"));
assert.ok(source.includes("app.use('/api', createSystemRouter"));
});
test('package.json 与 package-lock.json 顶层依赖同步', () => {
const packageJson = require('../package.json');
const packageLock = require('../package-lock.json');
assert.deepStrictEqual(packageLock.packages[''].dependencies, packageJson.dependencies);
});
test('前端 package.json 与 lock 文件依赖同步且没有手工缓存版本号', () => {
const frontendPackage = require('../../frontend/package.json');
const frontendLock = require('../../frontend/package-lock.json');
assert.deepStrictEqual(frontendLock.packages[''].dependencies, frontendPackage.dependencies);
assert.deepStrictEqual(frontendLock.packages[''].devDependencies, frontendPackage.devDependencies);
const frontendDirectory = path.join(projectRoot, 'frontend');
for (const fileName of fs.readdirSync(frontendDirectory).filter((name) => name.endsWith('.html'))) {
const html = fs.readFileSync(path.join(frontendDirectory, fileName), 'utf8');
assert.strictEqual(/\?v=\d+/.test(html), false, fileName);
}
});
test('生产部署统一使用 Vite dist 且不会删除 lock 文件', () => {
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
const nginxConfig = fs.readFileSync(path.join(projectRoot, 'nginx', 'nginx.conf'), 'utf8');
assert.ok(installer.includes('npm ci --include=dev'));
assert.ok(installer.includes('npm run build'));
assert.ok(installer.includes('frontend/dist'));
assert.strictEqual(/rm\s+-rf\s+node_modules\s+package-lock\.json/.test(installer), false);
assert.ok(nginxConfig.includes('root /usr/share/nginx/html;'));
assert.ok(nginxConfig.includes('alias /runtime/downloads/;'));
});
test('install.sh 生成的 Nginx server 块花括号平衡', () => {
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
const templates = [...installer.matchAll(/cat > .*?<< EOF\r?\n([\s\S]*?)\r?\nEOF/g)]
.map((match) => match[1])
.filter((template) => template.includes('server {'));
assert.strictEqual(templates.length, 3);
for (const template of templates) {
const openingBraces = (template.match(/\{/g) || []).length;
const closingBraces = (template.match(/\}/g) || []).length;
assert.strictEqual(openingBraces, closingBraces);
}
});
test('PM2 部署没有启用 cluster 或多实例参数', () => {
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
assert.strictEqual(/pm2\s+start[^\n]*(?:\s-i\s|--instances)/.test(installer), false);
});
console.log('\n========================================');
console.log('测试总结');
console.log('========================================');
console.log(`通过: ${results.passed}`);
console.log(`失败: ${results.failed}`);
process.exit(results.failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,155 @@
const assert = require('assert');
const { normalizeNonNegativeInteger, parseBooleanLike } = require('../utils/coerce');
const {
buildDeviceName,
detectDeviceTypeFromUserAgent,
inferPlatformFromUserAgent,
normalizeClientIp,
normalizeClientType,
resolveClientType,
sanitizeDeviceText
} = require('../utils/device');
const { parseDownloadTrafficLine, parseDownloadTrafficLogTime } = require('../utils/oss-log-parser');
const { isPathInside, normalizeVirtualPath } = require('../utils/path-safety');
const { isPathWithinShare, isShareIpAllowed, isValidShareCode, parseShareIpWhitelist } = require('../utils/share');
const { buildHttpDownloadUrl, sanitizeHttpBaseUrl } = require('../utils/url');
const {
compareLooseVersion,
normalizeReleaseNotes,
normalizeSha256,
normalizeVersion
} = require('../utils/version');
const results = { passed: 0, failed: 0 };
function test(name, fn) {
try {
fn();
results.passed += 1;
console.log(` [PASS] ${name}`);
} catch (error) {
results.failed += 1;
console.error(` [FAIL] ${name}: ${error.message}`);
}
}
console.log('\n========== 领域纯函数测试 ==========\n');
test('版本号规范化与宽松比较', () => {
assert.strictEqual(normalizeVersion(' v1.2.3 '), 'v1.2.3');
assert.strictEqual(normalizeVersion('', '1.0.0'), '1.0.0');
assert.strictEqual(compareLooseVersion('v1.10.0', '1.9.9'), 1);
assert.strictEqual(compareLooseVersion('1.2', '1.2.0'), 0);
assert.strictEqual(compareLooseVersion('1.2.0', '2.0.0'), -1);
});
test('发布说明转义换行被还原', () => {
assert.strictEqual(normalizeReleaseNotes('first\\r\\nsecond\\n'), 'first\nsecond');
});
test('SHA256 只接受完整十六进制摘要', () => {
assert.strictEqual(normalizeSha256(' A'.repeat(64)), '');
assert.strictEqual(normalizeSha256('A'.repeat(64)), 'a'.repeat(64));
assert.strictEqual(normalizeSha256('a'.repeat(63)), '');
});
test('非负整数与布尔配置规范化', () => {
assert.strictEqual(normalizeNonNegativeInteger(12.9), 12);
assert.strictEqual(normalizeNonNegativeInteger(-1, 4), 4);
assert.strictEqual(parseBooleanLike('YES'), true);
assert.strictEqual(parseBooleanLike('off', true), false);
assert.strictEqual(parseBooleanLike('invalid', true), true);
});
test('物理路径边界不接受兄弟目录', () => {
const parent = require('path').resolve('/tmp/root');
assert.strictEqual(isPathInside(parent, require('path').join(parent, 'child')), true);
assert.strictEqual(isPathInside(parent, require('path').resolve('/tmp/root-other')), false);
});
test('虚拟路径统一分隔符并拒绝遍历和空字节', () => {
assert.strictEqual(normalizeVirtualPath('folder\\file.txt'), '/folder/file.txt');
assert.strictEqual(normalizeVirtualPath('/folder/../secret'), null);
assert.strictEqual(normalizeVirtualPath('/folder/%2e%2e/secret'), null);
assert.strictEqual(normalizeVirtualPath('/folder/%2500.txt'), null);
assert.strictEqual(normalizeVirtualPath('/'), '/');
});
test('HTTP 基础地址拒绝危险协议并移除查询参数', () => {
assert.strictEqual(sanitizeHttpBaseUrl('javascript:alert(1)'), null);
assert.strictEqual(sanitizeHttpBaseUrl('https://example.test/base/?token=x#hash'), 'https://example.test/base');
});
test('下载地址逐段编码且不继承查询参数', () => {
assert.strictEqual(
buildHttpDownloadUrl('https://example.test/base?token=x', '/目录/a b.txt'),
'https://example.test/base/%E7%9B%AE%E5%BD%95/a%20b.txt'
);
});
test('OSS 日志时间正确应用时区', () => {
const date = parseDownloadTrafficLogTime('[27/Jul/2026:12:34:56 +0800]');
assert.strictEqual(date.toISOString(), '2026-07-27T04:34:56.000Z');
assert.strictEqual(parseDownloadTrafficLogTime('[31/Feb/2026:12:00:00 +0800]'), null);
assert.strictEqual(parseDownloadTrafficLogTime('[27/Jul/2026:12:00:00 +2460]'), null);
});
test('OSS 下载日志解析用户、对象键、字节和时间', () => {
const line = '1.2.3.4 - - [27/Jul/2026:12:34:56 +0800] "GET /user_42/folder/a%20b.txt?signature=x HTTP/1.1" 206 1024 "-" "ua"';
const result = parseDownloadTrafficLine(line);
assert.strictEqual(result.userId, 42);
assert.strictEqual(result.bytes, 1024);
assert.strictEqual(result.objectKey, 'user_42/folder/a b.txt');
assert.strictEqual(result.eventAt.toISOString(), '2026-07-27T04:34:56.000Z');
});
test('OSS 日志只统计成功 GET 下载', () => {
const fallback = new Date('2026-01-01T00:00:00Z');
assert.strictEqual(parseDownloadTrafficLine('"HEAD /user_1/a HTTP/1.1" 200 10', fallback), null);
assert.strictEqual(parseDownloadTrafficLine('"GET /user_1/a HTTP/1.1" 404 10', fallback), null);
assert.strictEqual(parseDownloadTrafficLine('"GET /public/a HTTP/1.1" 200 10', fallback), null);
assert.strictEqual(parseDownloadTrafficLine('"GET /user_1/a HTTP/1.1" 200 -', fallback), null);
});
test('分享码与分享路径严格限定', () => {
assert.strictEqual(isValidShareCode('Abc123'), true);
assert.strictEqual(isValidShareCode('../bad'), false);
assert.strictEqual(isPathWithinShare('/folder/a.txt', { share_type: 'folder', share_path: '/folder' }), true);
assert.strictEqual(isPathWithinShare('/folder-other/a.txt', { share_type: 'folder', share_path: '/folder' }), false);
assert.strictEqual(isPathWithinShare('/folder/a.txt', { share_type: 'file', share_path: '/folder/a.txt' }), true);
assert.strictEqual(isPathWithinShare('/folder/b.txt', { share_type: 'file', share_path: '/folder/a.txt' }), false);
});
test('IP 白名单支持分隔符、精确匹配与前缀规则', () => {
const whitelist = parseShareIpWhitelist('127.0.0.1, 10.0.*;192.168.1.5');
assert.deepStrictEqual(whitelist, ['127.0.0.1', '10.0.*', '192.168.1.5']);
assert.strictEqual(isShareIpAllowed('10.0.2.3', whitelist), true);
assert.strictEqual(isShareIpAllowed('192.168.1.6', whitelist), false);
assert.strictEqual(isShareIpAllowed('', whitelist), false);
assert.strictEqual(isShareIpAllowed('', []), true);
});
test('客户端 IP 与设备类型被稳定规范化', () => {
assert.strictEqual(normalizeClientIp('::ffff:127.0.0.1'), '127.0.0.1');
assert.strictEqual(normalizeClientIp('::1'), '127.0.0.1');
assert.strictEqual(detectDeviceTypeFromUserAgent('Mozilla/5.0 iPhone'), 'mobile');
assert.strictEqual(inferPlatformFromUserAgent('Mozilla/5.0 (Windows NT 10.0)'), 'Windows');
assert.strictEqual(normalizeClientType(' DESKTOP '), 'desktop');
assert.strictEqual(normalizeClientType('bot'), '');
assert.strictEqual(resolveClientType('', 'wanwan-cloud-desktop/1.0'), 'desktop');
});
test('设备名称限制长度并按客户端类型回退', () => {
assert.strictEqual(sanitizeDeviceText(' abc ', 2), 'ab');
assert.strictEqual(buildDeviceName({ clientType: 'mobile', platform: 'iOS' }), '移动端浏览器 · iOS');
assert.strictEqual(buildDeviceName({ clientType: 'web', deviceName: ' 我的设备 ', platform: 'Windows' }), '我的设备');
});
console.log('\n========================================');
console.log('测试总结');
console.log('========================================');
console.log(`通过: ${results.passed}`);
console.log(`失败: ${results.failed}`);
process.exit(results.failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,180 @@
process.env.LOG_LEVEL = 'silent';
const assert = require('assert');
const { EventEmitter } = require('events');
const fs = require('fs');
const {
createCsrfCookieMiddleware,
csrfProtection,
generateCsrfToken,
tokensEqual
} = require('../middleware/csrf');
const { RateLimiter } = require('../middleware/rate-limit');
const { applySecurityHeaders, createHttpsEnforcementMiddleware } = require('../middleware/security');
const { expressErrorHandler } = require('../middleware/error-handler');
const { requestContextMiddleware, redactLogValue } = require('../utils/logger');
const { createHealthService, normalizeMinimumFreeBytes } = require('../services/health-service');
const results = { passed: 0, failed: 0 };
function test(name, fn) {
try {
fn();
results.passed += 1;
console.log(` [PASS] ${name}`);
} catch (error) {
results.failed += 1;
console.error(` [FAIL] ${name}: ${error.message}`);
}
}
function createResponse() {
const response = new EventEmitter();
response.headers = {};
response.statusCode = 200;
response.payload = null;
response.cookies = [];
response.setHeader = (name, value) => { response.headers[name] = value; };
response.removeHeader = (name) => { delete response.headers[name]; };
response.status = (code) => { response.statusCode = code; return response; };
response.json = (payload) => { response.payload = payload; return response; };
response.cookie = (name, value, options) => { response.cookies.push({ name, value, options }); };
return response;
}
console.log('\n========== 中间件与可观测性测试 ==========\n');
test('安全响应头覆盖 CSP、HSTS、Referrer 与 Permissions Policy', () => {
const response = createResponse();
applySecurityHeaders({ secure: true }, response);
assert.ok(response.headers['Content-Security-Policy'].includes("object-src 'none'"));
assert.ok(response.headers['Strict-Transport-Security']);
assert.strictEqual(response.headers['X-Content-Type-Options'], 'nosniff');
assert.strictEqual(response.headers['Referrer-Policy'], 'strict-origin-when-cross-origin');
assert.ok(response.headers['Permissions-Policy']);
});
test('HTTPS 中间件阻止不安全请求但放行本机健康检查', () => {
const middleware = createHttpsEnforcementMiddleware({ enabled: true });
const blockedResponse = createResponse();
middleware({ path: '/api/config', secure: false, socket: { remoteAddress: '10.0.0.1' }, id: 'req-1' }, blockedResponse, () => assert.fail('不应放行'));
assert.strictEqual(blockedResponse.statusCode, 400);
assert.strictEqual(blockedResponse.payload.requestId, 'req-1');
let nextCalled = false;
middleware({ path: '/api/health', secure: false, socket: { remoteAddress: '127.0.0.1' } }, createResponse(), () => { nextCalled = true; });
assert.strictEqual(nextCalled, true);
});
test('请求上下文接受安全 requestId 并回写响应头', () => {
const request = { headers: { 'x-request-id': 'trace-123' }, method: 'GET', path: '/api/config' };
const response = createResponse();
requestContextMiddleware(request, response, () => {});
assert.strictEqual(request.id, 'trace-123');
assert.strictEqual(response.headers['X-Request-ID'], 'trace-123');
response.emit('finish');
});
test('非法 requestId 被替换,日志敏感字段被脱敏', () => {
const request = { headers: { 'x-request-id': 'bad id\n' }, method: 'GET', path: '/' };
const response = createResponse();
requestContextMiddleware(request, response, () => {});
assert.notStrictEqual(request.id, 'bad id\n');
assert.match(request.id, /^[0-9a-f-]{36}$/);
const redacted = redactLogValue({ password: 'secret', note: 'token=abc', nested: { authorization: 'Bearer value' } });
assert.strictEqual(redacted.password, '[REDACTED]');
assert.strictEqual(redacted.note, 'token=[REDACTED]');
assert.strictEqual(redacted.nested.authorization, '[REDACTED]');
response.emit('finish');
});
test('CSRF token 使用固定长度并支持常量时间比较', () => {
const token = generateCsrfToken();
assert.match(token, /^[a-f0-9]{64}$/);
assert.strictEqual(tokensEqual(token, token), true);
assert.strictEqual(tokensEqual(token, `${token}0`), false);
});
test('CSRF Cookie 中间件只在缺少 token 时设置 Cookie', () => {
const middleware = createCsrfCookieMiddleware({ secureCookies: true });
const response = createResponse();
middleware({ cookies: {} }, response, () => {});
assert.strictEqual(response.cookies.length, 1);
assert.strictEqual(response.cookies[0].options.secure, true);
assert.strictEqual(response.cookies[0].options.sameSite, 'strict');
});
test('Cookie 会话的写请求必须通过 CSRF 校验', () => {
const response = createResponse();
csrfProtection({
method: 'POST',
path: '/api/example',
id: 'req-2',
cookies: { token: 'session', csrf_token: 'a' },
headers: { 'x-csrf-token': 'b' }
}, response, () => assert.fail('不应放行'));
assert.strictEqual(response.statusCode, 403);
assert.strictEqual(response.payload.requestId, 'req-2');
});
test('失败限流器达到阈值后封锁并可在成功后清除', () => {
const limiter = new RateLimiter({ maxAttempts: 2, windowMs: 1000, blockDuration: 1000 });
try {
assert.strictEqual(limiter.recordFailure('key').blocked, false);
assert.strictEqual(limiter.recordFailure('key').blocked, true);
assert.strictEqual(limiter.isBlocked('key'), true);
limiter.recordSuccess('key');
assert.strictEqual(limiter.isBlocked('key'), false);
} finally {
limiter.destroy();
}
});
test('健康检查同时验证数据库与磁盘', () => {
const service = createHealthService({
db: { prepare: () => ({ get: () => ({ ok: 1 }) }) },
diskPath: __dirname,
minimumFreeBytes: 0
});
const result = service.check();
assert.strictEqual(result.healthy, true);
assert.strictEqual(result.checks.database.status, 'ok');
assert.strictEqual(result.checks.disk.status, 'ok');
assert.ok(result.checks.disk.freeBytes > 0);
assert.strictEqual(fs.existsSync(__dirname), true);
assert.strictEqual(normalizeMinimumFreeBytes(-1), 256 * 1024 * 1024);
});
test('数据库异常会让健康状态降级', () => {
const service = createHealthService({
db: { prepare: () => { throw Object.assign(new Error('down'), { code: 'SQLITE_DOWN' }); } },
diskPath: __dirname,
minimumFreeBytes: 0
});
const result = service.check();
assert.strictEqual(result.healthy, false);
assert.strictEqual(result.checks.database.status, 'error');
});
test('错误响应携带 requestId 并隐藏服务端异常', () => {
const response = createResponse();
expressErrorHandler(new Error('secret detail'), {
id: 'req-3',
method: 'GET',
path: '/api/example'
}, response, () => assert.fail('不应继续'));
assert.deepStrictEqual(response.payload, {
success: false,
message: '服务器内部错误',
requestId: 'req-3'
});
});
console.log('\n========================================');
console.log('测试总结');
console.log('========================================');
console.log(`通过: ${results.passed}`);
console.log(`失败: ${results.failed}`);
process.exit(results.failed > 0 ? 1 : 0);

View File

@@ -1,3 +1,5 @@
process.env.LOG_LEVEL = 'silent';
const assert = require('assert');
const {
parseDateTimeValue,

View File

@@ -8,6 +8,9 @@ const path = require('path');
const testFiles = [
'boundary-tests.js',
'production-utils-tests.js',
'domain-utils-tests.js',
'middleware-tests.js',
'architecture-tests.js',
'archive-tests.js',
'network-concurrent-tests.js',
'state-consistency-tests.js'