- frontend-architecture-refactor: 拆分 hooks/组件、类型筛选器动态化 - http-checker-quality-hardening: ReDoS 防护、failure 格式修正、测试补全
5.6 KiB
Context
HTTP checker 是 DiAL 拨测系统的核心 runner 之一,负责对 HTTP 目标执行请求并校验响应。经审查发现以下质量问题:
- actual 值截断格式不符合 spec:spec 要求 failure 中的 actual 摘要需截断并附带字符计数,但当前
truncateActual函数只加省略号无计数,导致用户无法判断原始响应体规模。 - ReDoS 风险:用户配置的 regex body 规则和 match operator 直接对大响应体执行
new RegExp().test(),恶意或不当正则可能导致 CPU 阻塞。 - JSON 重复解析:多条 json body 规则各自独立调用
JSON.parse(body),对大 JSON 响应体造成不必要的重复开销。 - CSS 规则分支冗余:
checkCssRule中"无 operator 时检查元素存在"和"exists: true"是重复逻辑。 - 重定向测试不足:303、307/308、相对路径 Location 等分支缺少测试覆盖。
当前代码结构:
src/server/checker/expect/failure.ts— failure 构造函数src/server/checker/runner/http/body.ts— body 规则检查src/server/checker/runner/http/execute.ts— HTTP 执行主流程src/server/checker/expect/operator.ts— operator 匹配逻辑
Goals / Non-Goals
Goals:
- 实现 failure actual 值截断,满足 spec 要求
- 消除 regex 相关的 ReDoS 风险
- 优化多条 JSON 规则的解析性能
- 精简冗余代码分支
- 补全重定向和集成测试覆盖
Non-Goals:
- 不改变 CheckResult / CheckFailure 的类型结构(截断在构造时完成,对外接口不变)
- 不引入新依赖
- 不改变 HTTP checker 的功能行为(纯内部质量改进)
- 不添加 response timing 分段记录(暂缓)
- 不添加重试机制(拨测场景下重试会掩盖网络问题信号)
Decisions
Decision 1: actual 截断在 mismatchFailure 构造点统一实施
选择:在 expect/failure.ts 的 mismatchFailure 函数内部对 actual 参数截断,阈值 200 字符。
替代方案:
- 在存储层(store.ts insertCheckResult)截断 — 但这样 API 实时返回的 failure 仍然很大
- 在每个调用点手动截断 — 分散且容易遗漏
理由:构造点截断是最集中的拦截位置,所有 mismatch failure 都经过此函数,一处修改全局生效。expected 值不截断(来自用户配置,通常很短)。
截断格式:<前 200 字符>…(共 N 字符) — 保留前缀便于诊断,附带总长度便于判断规模(省略号为单字符 U+2026)。
Decision 2: ReDoS 防护使用正则复杂度静态检测
选择:在启动期 validate 阶段对 regex body 规则和 match operator 进行静态复杂度检测,拒绝含有嵌套量词等危险模式的正则。运行期不做额外防护。
替代方案:
- 运行期用 AbortSignal + setTimeout 强制中断 — Bun 的 RegExp 执行不可中断,无法实现
- 使用 safe-regex 库 — 引入新依赖,违反项目规范
- 限制正则执行的输入长度 — 会影响正常大响应体的匹配
理由:自行实现轻量级检测函数,检查常见 ReDoS 模式(嵌套量词 (a+)+、重叠交替 (a|a)*)。在 validate 阶段拒绝危险正则,比运行期防护更可靠——配置错误应该在启动时暴露。
检测规则:
- 嵌套量词:量词内包含量词(如
(a+)+、(a*)*、(a+)*) - 重叠字符类交替后跟量词:
(x|x)+模式
Decision 3: JSON parse 结果缓存在 checkBodyExpect 层
选择:在 checkBodyExpect 函数中,首次遇到 json 规则时执行 JSON.parse,将结果缓存并传递给后续 json 规则复用。
实现方式:修改 checkSingleBodyRule 签名,接受可选的 parsedJson 参数;在 checkBodyExpect 循环中维护一个 let parsedJson: { ok: boolean; value?: unknown; error?: string } 状态。
理由:改动最小,不改变外部接口,只在内部传递缓存。对于非 json 规则(contains、regex、css、xpath)无影响。
Decision 4: CSS 规则分支合并策略
选择:将 checkCssRule 重构为线性流程:
- 解析 HTML
- 处理
exists: false(元素不存在即通过) - 查找元素(不存在则失败)
- 处理
exists: true(到这里已确认存在,直接通过) - 提取值(attr 或 text)
- 无 operator 时检查值非 undefined 即通过
- 有 operator 时执行匹配
理由:消除当前三层嵌套判断中的重复逻辑,使控制流线性化,更易理解和维护。
Decision 5: execute.ts 提前 duration 检查保留但加注释
选择:保留第 56-74 行的提前 duration 检查逻辑(它是有效的性能优化——避免读取注定超时的 body),但重构为独立的 helper 函数使意图更明确。
理由:删除它会导致超时场景下仍然读取完整 body 后才报错,浪费网络带宽和时间。提取为 checkEarlyTimeout 函数名即可自解释。
Risks / Trade-offs
- ReDoS 静态检测的误报:过于严格的检测可能拒绝合法但看起来复杂的正则。→ 缓解:只检测最常见的嵌套量词模式,不做过度分析;提供清晰的错误信息指导用户修改。
- actual 截断丢失诊断信息:截断后用户无法看到完整 actual 值。→ 缓解:200 字符的前缀通常足够定位问题;如需完整响应体,用户应直接请求目标 URL 查看。
- JSON parse 缓存的内存占用:对于大 JSON 响应体,缓存的 parsed 对象会在整个 body rules 检查期间驻留内存。→ 缓解:这是短暂的(单次检查周期内),且原本每条规则都会各自 parse 一份,缓存反而减少了峰值内存。