diff --git a/docs/changelog/index.md b/docs/changelog/index.md index b0ff7ce2..3dd21af1 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -23,7 +23,7 @@ sidebar: false ### 新增 -- WAF 规则编排新增「UA 检查」节点:可要求携带 User-Agent、按浏览器/操作系统白名单(且/或)匹配,并优先屏蔽常见爬虫与非正常 UA。 +- WAF 规则编排新增「UA 检查」节点:可要求携带 User-Agent、按浏览器/操作系统白名单(且/或)匹配,并优先屏蔽常见爬虫、非正常 UA(不含爬虫)与自定义正则 UA。 ### 改进 diff --git a/docs/design/waf-orchestration-design.md b/docs/design/waf-orchestration-design.md index cd18d029..7e95703f 100644 --- a/docs/design/waf-orchestration-design.md +++ b/docs/design/waf-orchestration-design.md @@ -15,7 +15,7 @@ | 阻止 | 可创建多个 | 一个或多个 | 无 | HTTP 状态码、HTML 响应体 | | IP 匹配 | 可创建多个 | 一个或多个 | `true`、`false` | IP、CIDR、IP 组 ID | | 地域匹配 | 可创建多个 | 一个或多个 | `true`、`false` | 国家代码、地区代码 | -| UA 检查 | 可创建多个 | 一个或多个 | `true`、`false` | 要求携带 UA、浏览器/OS 白名单与 and/or、屏蔽爬虫/非正常 UA | +| UA 检查 | 可创建多个 | 一个或多个 | `true`、`false` | 要求携带 UA、浏览器/OS 白名单与 and/or、屏蔽爬虫/非正常 UA(不含爬虫)/自定义正则 | | PoW | 可创建多个 | 一个或多个 | `next` | 算法、难度、会话 TTL、挑战 TTL | IP 匹配、地域匹配与 UA 检查不区分黑名单或白名单。`true` 只表示请求通过该节点判定,`false` 只表示未通过;放行或阻止的业务含义完全由连线决定。UA 检查的求值顺序为:要求携带 UA → 屏蔽爬虫/非正常 UA → 白名单匹配。PoW 验证完成后沿 `next` 继续,未完成时由挑战页面接管当前请求,不产生 `false` 分支。 diff --git a/docs/superpowers/specs/2026-07-19-waf-ua-check-node-design.md b/docs/superpowers/specs/2026-07-19-waf-ua-check-node-design.md index d7478407..9d08b2bf 100644 --- a/docs/superpowers/specs/2026-07-19-waf-ua-check-node-design.md +++ b/docs/superpowers/specs/2026-07-19-waf-ua-check-node-design.md @@ -47,7 +47,9 @@ "operating_systems": [], "match_mode": "or", "block_common_bots": false, - "block_abnormal_ua": false + "block_abnormal_ua": false, + "block_custom_ua": false, + "custom_ua_patterns": [] } ``` @@ -58,7 +60,9 @@ | `operating_systems` | string[] | 白名单操作系统标签;空表示不限制 OS | | `match_mode` | `"and"` \| `"or"` | **浏览器条件与 OS 条件**之间的组合;默认 `"or"` | | `block_common_bots` | bool | 屏蔽常见爬虫:分类 browser 或 os 为 `Bot` → **false** | -| `block_abnormal_ua` | bool | 屏蔽非正常 UA:browser ∈ `{Bot, Other, Unknown}` → **false** | +| `block_abnormal_ua` | bool | 屏蔽非正常 UA:browser ∈ `{Other, Unknown}`(**不含** Bot/搜索引擎爬虫)→ **false** | +| `block_custom_ua` | bool | 屏蔽自定义 UA:原始 UA 命中 `custom_ua_patterns` 任一条 → **false** | +| `custom_ua_patterns` | string[] | 正则列表(边缘为 Lua 模式);开启 `block_custom_ua` 时至少一条 | 默认值:开关全 `false`,列表空,`match_mode: "or"`。 @@ -85,20 +89,21 @@ 1) if require_ua and ua 为空 → false 2) browser, os := classify(ua) 3) if block_common_bots and (browser == "Bot" or os == "Bot") → false -4) if block_abnormal_ua and browser in {"Bot","Other","Unknown"} → false -5) has_browsers := browsers 非空; has_os := operating_systems 非空 -6) if not has_browsers and not has_os → true -7) browser_hit := browser ∈ browsers; os_hit := os ∈ operating_systems -8) if has_browsers and not has_os → browser_hit -9) if has_os and not has_browsers → os_hit -10) if both lists set: +4) if block_abnormal_ua and browser in {"Other","Unknown"} → false +5) if block_custom_ua and UA matches any custom_ua_patterns → false +6) has_browsers := browsers 非空; has_os := operating_systems 非空 +7) if not has_browsers and not has_os → true +8) browser_hit := browser ∈ browsers; os_hit := os ∈ operating_systems +9) if has_browsers and not has_os → browser_hit +10) if has_os and not has_browsers → os_hit +11) if both lists set: match_mode == "and" → browser_hit and os_hit match_mode == "or" → browser_hit or os_hit ``` 说明: -- **屏蔽优先于匹配**:步骤 3–4 在白名单之前。 +- **屏蔽优先于匹配**:步骤 3–5 在白名单之前。 - **未配置匹配列表**:步骤 6 直接 true(仅受 require / block 约束)。 - **仅一侧列表有值**:只校验该侧是否命中;`match_mode` 仅在两侧都有值时生效。 - 节点本身不 allow/block,仅选句柄;下游连线决定动作。 diff --git a/frontend/app/(main)/waf/rules/editor/components/graph-validation.ts b/frontend/app/(main)/waf/rules/editor/components/graph-validation.ts index 177e0bff..cb61819f 100644 --- a/frontend/app/(main)/waf/rules/editor/components/graph-validation.ts +++ b/frontend/app/(main)/waf/rules/editor/components/graph-validation.ts @@ -218,6 +218,23 @@ function validateNodeConfig(node: WAFRuleNode): string | undefined { return `节点 ${node.id} 包含无效浏览器标签`; if (node.config.operating_systems.some((label) => !UA_OS_LABELS.has(label))) return `节点 ${node.id} 包含无效操作系统标签`; + if (node.config.custom_ua_patterns.length > 32) + return `节点 ${node.id} 的自定义 UA 正则不能超过 32 条`; + for (const pattern of node.config.custom_ua_patterns) { + if (!pattern.trim()) return `节点 ${node.id} 的自定义 UA 正则不能为空`; + if (new TextEncoder().encode(pattern).length > 256) + return `节点 ${node.id} 的自定义 UA 正则过长`; + try { + void new RegExp(pattern); + } catch { + return `节点 ${node.id} 的自定义 UA 正则无效`; + } + } + if ( + node.config.block_custom_ua && + node.config.custom_ua_patterns.length === 0 + ) + return `节点 ${node.id} 开启屏蔽自定义 UA 时至少需要一条正则`; } return undefined; } diff --git a/frontend/app/(main)/waf/rules/editor/components/node-factory.test.ts b/frontend/app/(main)/waf/rules/editor/components/node-factory.test.ts index 7c7c2c39..2255c562 100644 --- a/frontend/app/(main)/waf/rules/editor/components/node-factory.test.ts +++ b/frontend/app/(main)/waf/rules/editor/components/node-factory.test.ts @@ -66,6 +66,8 @@ describe('createRuleNode ua_check', () => { match_mode: 'or', block_common_bots: false, block_abnormal_ua: false, + block_custom_ua: false, + custom_ua_patterns: [], }); } }); diff --git a/frontend/app/(main)/waf/rules/editor/components/node-factory.ts b/frontend/app/(main)/waf/rules/editor/components/node-factory.ts index 37f2d007..2e38f530 100644 --- a/frontend/app/(main)/waf/rules/editor/components/node-factory.ts +++ b/frontend/app/(main)/waf/rules/editor/components/node-factory.ts @@ -50,6 +50,8 @@ export function createRuleNode( match_mode: 'or', block_common_bots: false, block_abnormal_ua: false, + block_custom_ua: false, + custom_ua_patterns: [], }, }; if (type === 'pow') diff --git a/frontend/app/(main)/waf/rules/editor/components/node-properties.test.tsx b/frontend/app/(main)/waf/rules/editor/components/node-properties.test.tsx index bb4f8483..9d57b53b 100644 --- a/frontend/app/(main)/waf/rules/editor/components/node-properties.test.tsx +++ b/frontend/app/(main)/waf/rules/editor/components/node-properties.test.tsx @@ -17,6 +17,8 @@ it('hides match and block until UA check is enabled', () => { match_mode: 'or', block_common_bots: false, block_abnormal_ua: false, + block_custom_ua: false, + custom_ua_patterns: [], }, }; const onChange = vi.fn(); @@ -40,12 +42,9 @@ it('hides match and block until UA check is enabled', () => { ); expect(screen.getByLabelText('屏蔽常见爬虫 UA')).toBeInTheDocument(); expect(screen.getByLabelText('屏蔽非正常 UA')).toBeInTheDocument(); - expect( - screen.getByText(/浏览器或操作系统分类为 Bot/), - ).toBeInTheDocument(); - expect( - screen.getByText(/浏览器分类为 Bot、Other 或 Unknown/), - ).toBeInTheDocument(); + expect(screen.getByLabelText('屏蔽自定义 UA')).toBeInTheDocument(); + expect(screen.getByText(/浏览器或操作系统分类为 Bot/)).toBeInTheDocument(); + expect(screen.getByText(/浏览器分类为 Other 或 Unknown/)).toBeInTheDocument(); }); it('edits display name for configurable nodes', () => { diff --git a/frontend/app/(main)/waf/rules/editor/components/node-properties.tsx b/frontend/app/(main)/waf/rules/editor/components/node-properties.tsx index 0b5cdda4..e62520e0 100644 --- a/frontend/app/(main)/waf/rules/editor/components/node-properties.tsx +++ b/frontend/app/(main)/waf/rules/editor/components/node-properties.tsx @@ -277,7 +277,8 @@ function PropertyFields({ 屏蔽非正常 UA - 浏览器分类为 Bot、Other 或 Unknown(无法识别为已知浏览器) + 浏览器分类为 Other 或 Unknown(不含搜索引擎等爬虫 + Bot,爬虫请用上方开关) + +
+ + 屏蔽自定义 UA + + + 原始 User-Agent 命中任一条正则时返回 false(Lua 模式语法) + +
+ + onChange({ + ...node, + config: { ...node.config, block_custom_ua }, + }) + } + /> +
+ {node.config.block_custom_ua && ( + + + 自定义 UA 正则 + +