#!/usr/bin/env node /** * verify-dev-server.mjs — 静态服务的健壮性验证(零依赖,只用 node 内置模块)。 * * 背景(实测缺陷 P1): * 修复前 `site/dev-server.js` 的请求回调直接调用 fs.readFile(filePath, cb), * 而 decodeURIComponent('%00') 会解出 NUL 字符;fs.readFile 对含 NUL 的路径**同步** * 抛 TypeError [ERR_INVALID_ARG_VALUE],异常逃逸出请求回调 → 整个进程退出。 * 即:单个畸形请求 = 一次 DoS。 * * 反例断言(修复前必然失败,用来区分修复前后): * [COUNTEREXAMPLE] MALFORMED_BURST_SERVER_ALIVE * —— 发完 %00 / 穿越 / 双重编码 NUL 请求后,子进程仍存活(exitCode === null), * 且再发一次正常请求仍为 200;stderr 不含 ERR_INVALID_ARG_VALUE。 * 修复前的真实表现(本机实测,修复前工作树副本): * GET /site/%00 -> 客户端 ECONNRESET * child.exitCode -> 1 * 其后 GET /site/ -> ECONNREFUSED * stderr: TypeError [ERR_INVALID_ARG_VALUE]: The argument 'path' must be a string, * Uint8Array, or URL without null bytes. * at Object.readFile (node:fs:385:16) * at Server. (.../site/dev-server.js:106:6) * * 端口:默认用 13311,避免与正在运行的 3311 实例抢端口;可用 KOLE_PORT 覆盖。 * 验证脚本把 KOLE_PORT 传给子进程(这也是 dev-server.js 新增的唯一开关)。 * * 运行:node tools/verify-dev-server.mjs * 退出码:0 = 全部通过;1 = 有断言失败 / 服务起不来。 */ import { spawn } from 'node:child_process'; import { request } from 'node:http'; import { join } from 'node:path'; const ROOT = join(import.meta.dirname, '..'); const HOST = '127.0.0.1'; const PORT = Number(process.env.KOLE_PORT) || 13311; const BASELINE_PORT = 13311; let failed = 0; let passed = 0; const results = []; function check(name, ok, detail = '') { results.push({ name, ok, detail }); if (ok) passed++; else failed++; } function get(path) { return new Promise((resolve) => { const req = request({ host: HOST, port: PORT, path, method: 'GET' }, (res) => { let body = ''; res.setEncoding('utf8'); res.on('data', (chunk) => { body += chunk; }); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body, error: null })); }); req.on('error', (e) => resolve({ status: 0, headers: {}, body: '', error: e.code || e.message })); req.setTimeout(5000, () => { req.destroy(); resolve({ status: 0, headers: {}, body: '', error: 'TIMEOUT' }); }); req.end(); }); } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); console.log(`[dev-server] 启动子进程 node site/dev-server.js (KOLE_PORT=${PORT}, HOST=${HOST})`); const child = spawn(process.execPath, [join('site', 'dev-server.js')], { cwd: ROOT, env: { ...process.env, KOLE_PORT: String(PORT) }, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout.on('data', (d) => { stdout += d; }); child.stderr.on('data', (d) => { stderr += d; }); const watchdog = setTimeout(() => { console.error('[dev-server] 看门狗超时(20s),强制收尾'); try { child.kill(); } catch { /* 已退出 */ } process.exit(1); }, 20000); /* ---------- 1. 等待就绪 ---------- */ let ready = null; for (let i = 0; i < 40; i++) { if (child.exitCode !== null) break; const r = await get('/site/'); if (r.status === 200) { ready = r; break; } await sleep(250); } check('server starts and answers GET /site/ with 200', ready !== null, ready ? `status=${ready.status}` : `子进程未就绪 exitCode=${child.exitCode} stderr=${JSON.stringify(stderr.slice(0, 200))}`); check('KOLE_PORT honored in startup banner', stdout.includes(`:${PORT}/site/`), `banner=${JSON.stringify(stdout.trim().split('\n')[0] || '')}`); if (ready) { check('GET /site/ serves the real site document', //i.test(ready.body) && ready.body.length > 200, `bytes=${ready.body.length}`); check('security headers kept (CSP + nosniff)', /default-src 'self'/.test(ready.headers['content-security-policy'] || '') && ready.headers['x-content-type-options'] === 'nosniff'); } /* ---------- 2. 既有行为回归(不得被本次修复改动) ---------- */ const rootRedirect = await get('/'); check('GET / -> 302 to /site/', rootRedirect.status === 302 && rootRedirect.headers.location === '/site/', `status=${rootRedirect.status} location=${rootRedirect.headers.location}`); const indexHtml = await get('/site/index.html'); check('GET /site/index.html -> 200 text/html', indexHtml.status === 200 && /text\/html/.test(indexHtml.headers['content-type'] || ''), `status=${indexHtml.status}`); const appJs = await get('/site/app.js'); check('GET /site/app.js -> 200 text/javascript', appJs.status === 200 && /text\/javascript/.test(appJs.headers['content-type'] || ''), `status=${appJs.status}`); const missing = await get('/site/definitely-missing-xyz.html'); check('missing file -> 404 with unchanged wording', missing.status === 404 && missing.body === '404 Not Found: /site/definitely-missing-xyz.html', `status=${missing.status}`); /* ---------- 3. 畸形请求矩阵(全部必须 4xx,且不得打挂进程) ---------- */ const malformed = [ ['/site/%00', 'NUL 编码'], ['/site/%2500', '双重编码 NUL(%2500)'], ['/site/%00.html', 'NUL 编码 + 后缀'], ['/site/sub/%2500', '子目录下的双重编码 NUL'], ['/site/../package.json', '相对穿越'], ['/..%2fpackage.json', '编码穿越 %2f'], ['/site/%2e%2e/package.json', '编码穿越 %2e'], ['/site/.git/config', '敏感段 .git'] ]; const malformedSeen = []; for (const [path, label] of malformed) { const r = await get(path); malformedSeen.push(`${path}(${label}) -> ${r.status || r.error}`); check(`malformed ${path} (${label}) -> 4xx`, r.status >= 400 && r.status < 500, `status=${r.status || r.error}`); } /* ---------- 4. 反例断言:修复前必然失败 ---------- */ await sleep(300); const aliveExitCode = child.exitCode; const afterBurst = await get('/site/'); check('[COUNTEREXAMPLE] MALFORMED_BURST_SERVER_ALIVE — 畸形请求突发后进程仍存活', aliveExitCode === null, `child.exitCode=${aliveExitCode}(修复前为 1)`); check('[COUNTEREXAMPLE] MALFORMED_BURST_STILL_SERVING — 突发后正常请求仍 200', afterBurst.status === 200, `status=${afterBurst.status || afterBurst.error}(修复前为 ECONNREFUSED)`); check('[COUNTEREXAMPLE] NO_FATAL_STDERR — stderr 不含 ERR_INVALID_ARG_VALUE 崩溃栈', !/ERR_INVALID_ARG_VALUE/.test(stderr), `stderr=${JSON.stringify(stderr.slice(0, 160))}`); check('repeated NUL requests stay handled (10x)', await (async () => { for (let i = 0; i < 10; i++) { const r = await get('/site/%00'); if (!(r.status >= 400 && r.status < 500)) return false; } const last = await get('/site/'); return last.status === 200 && child.exitCode === null; })(), '10 次 %00 后仍存活且仍 4xx'); /* ---------- 收尾 ---------- */ try { child.kill(); } catch { /* 已退出 */ } clearTimeout(watchdog); console.log(''); console.log(`[dev-server] 端口 ${PORT}${PORT === BASELINE_PORT ? '(默认验证端口,未占用 3311)' : '(KOLE_PORT 覆盖)'}`); console.log(`[dev-server] 畸形请求实测:${malformedSeen.join(' | ')}`); for (const r of results) { console.log(`[dev-server] ${r.ok ? 'OK' : 'FAIL'} ${r.name}${r.detail ? ` — ${r.detail}` : ''}`); } if (failed) { console.error(`\n[dev-server] ${failed} check(s) failed / ${passed} passed`); process.exit(1); } console.log(`\n[dev-server] OK — ${passed} checks passed(含 3 条修复前必失败的反例断言)`); process.exit(0);