diff --git a/api/src/send-chain/drainage-authorization.spec.ts b/api/src/send-chain/drainage-authorization.spec.ts index 8fb2035..e80e8a5 100644 --- a/api/src/send-chain/drainage-authorization.spec.ts +++ b/api/src/send-chain/drainage-authorization.spec.ts @@ -30,6 +30,28 @@ const material = (id: string, url: string, channels: string[], auditStatus = 'ap })), }); describe('drainage authorization', () => { + it.each(['详情:https://qa0915.example.com', '详情:https://qa0915.example.com'])( + 'keeps prose separators outside explicit URLs: %s', + (content) => { + const text = 'https://qa0915.example.com'; + const start = content.indexOf(text); + const targets = drainageTargets(content, [ + { + ruleId: 'url', + ruleCode: 'URL', + ruleName: 'URL', + category: 'url', + text, + normalizedText: text, + start, + end: start + text.length, + }, + ]); + expect(targets).toHaveLength(1); + expect(targets[0].text).toBe(text); + expect(materialMatches(targets[0], text)).toBe(true); + }, + ); test.each([ 'lisglo.cn', 'sms.lisglo.cn', diff --git a/api/src/send-chain/drainage-authorization.ts b/api/src/send-chain/drainage-authorization.ts index 0deb369..7b3d30c 100644 --- a/api/src/send-chain/drainage-authorization.ts +++ b/api/src/send-chain/drainage-authorization.ts @@ -81,7 +81,11 @@ export function drainageTargets(content: string, matches: DrainageDetectionMatch if (start < 0 || end <= 0) throw new ServiceUnavailableException('引流识别位置无效'); // Extend the entire URL token, including suffix labels, userInfo and query. const token = /[a-z0-9:/?&=.%_+@#~!$*()[\]-]/i; - while (start > 0 && token.test(normalized.text[start - 1])) start--; + while (start > 0 && token.test(normalized.text[start - 1])) { + // A prose colon before an explicit scheme is a separator, including normalized Chinese colons. + if (normalized.text[start - 1] === ':' && /^https?:\/\//i.test(normalized.text.slice(start, end))) break; + start--; + } while (end < normalized.text.length && token.test(normalized.text[end])) end++; const text = normalized.text.slice(start, end).replace(/[.,;!]+$/, ''); const value = drainageHost(text); diff --git a/docs/drainage-send-gating-plan-20260910.md b/docs/drainage-send-gating-plan-20260910.md index 8990a07..b8749e6 100644 --- a/docs/drainage-send-gating-plan-20260910.md +++ b/docs/drainage-send-gating-plan-20260910.md @@ -193,3 +193,8 @@ Gateway 每个分片等待可用连接后通过仅本机直连的 POST /api/gate ### 11.1 实际数据库约束补核 真实隔离库复现原ChannelSignatureReportTask_drainage_target_key是签名+引流+通道部分唯一索引(Prisma模型未声明此部分约束)。必须新增20260914093000_drainage_carrier_reports,在同事务建立carrier非空四维唯一索引及carrier为空历史三维唯一索引,再移除旧三维索引;不改旧数据/状态。新索引同样防止状态保存与导出并发产生重复任务。迁移仅在独立验收库执行,发布后方能启用新代码;不能回退旧程序继续发送并把三网任务当通道级读取。应用回退需暂停发送并评估三网事实,不能删除新任务或直接重建旧唯一索引。 + + +### 2026-09-15 URL 前置文字分隔符补充 + +显式 http(s) URL 前紧邻的中英文冒号属于正文分隔符,不得进入引流域名解析。URL 内的协议、userinfo、完整域名后缀及 query 仍须整体校验,不能通过缩短匹配令恶意域名借用已报备资料。真实误拦截与回归记录见 HTTP-0915-B03。 diff --git a/docs/system-functional-test-cases.md b/docs/system-functional-test-cases.md index dea8213..abadc41 100644 --- a/docs/system-functional-test-cases.md +++ b/docs/system-functional-test-cases.md @@ -5550,3 +5550,9 @@ CLIENT-0914-01~07 的模板样式/顺序、文档归属与检索、中文状 - HTTP-0915-B01:上行mobile=abc/1/空/超长,accessNumber=非数字/空/22位返回400;合法11位手机号和1–21位接入号精确匹配。 - HTTP-0915-B02:仅HTTP开通、CMPP关闭时合法请求正常送达;HTTP总开关或sendEnabled关闭仍拒绝,客户端/CMPP原关闭检查保留;外部sourceType不能模拟内部HTTP上下文。 - HTTP-0915-E2E:四公开接口鉴权/nonce/过期/签名篡改/参数类型及边界/幂等/重复/隔离/分页;专用模拟通道正常与失败回执、黑名单/号码频次/模板/签名/引流/敏感词拦截,核对真实PG、Redis、Gateway、Webhook签名/成功/重试/终止及扣退费;测试后排空并停用本轮对象。每项执行结论写进度和证据矩阵,不把旧版本通过当新版本通过。 + + +### HTTP-0915-B03:引流 URL 前置分隔符 + +- 已审核签名、模板、引流资料及当前运营商通道报备通过时,正文中的“详情:https://qa0915.example.com”和英文冒号版本应提取完整 URL,正常路由;冒号不计入 URL。 +- 未登记、未审核、当前运营商报备失败仍应分别拒绝,零供应商 Submit;恶意后缀、userinfo 和 query 不得借用其他域名报备。 diff --git a/docs/testing-progress.md b/docs/testing-progress.md index 5a81993..af2365e 100644 --- a/docs/testing-progress.md +++ b/docs/testing-progress.md @@ -5042,3 +5042,12 @@ CUA本轮可用,实际后端文档三尺寸1600×1000/1366×768/390×844无页 用户明确授权仅专用企业、应用、模拟通道及号码发送/回执/Webhook和必要测试配置。已新建HTTP隔离0915企业、2应用、3条远端loopback模拟通道、测试余额及对应流水;原客户/通道无修改。模拟器仅接受指定测试账号与号码、最多40个Submit,不连接运营商。当前准备上线新代码后完成真实链路复验及测试收尾。证据.local-data/http-complete-20260915。 用户要求安全复用sudo密码:已存当前Windows用户DPAPI加密文件(仓库外),不写明文;本机受保护未跟踪release.py仅新增test专用--sudo-credential及测试,47项工具测试含5项平台跳过。发现PowerShell7模块路径影响PowerShell5解密,调用时移除继承PSModulePath后真实只读SSH通过。工具变更后新计划绑定新摘要,不复用旧preflight。工具仍未提交,不能以应用SHA冒充工具版本。后续发布/推送及完成证据另行追加;预生产不在范围。 + + +### 2026-09-15 HTTP 全链路复测:引流 URL 冒号边界误拦截(进行中) + +- 测试环境 c781313 的 78 项异常参数、新签名及旧协议拒绝用例全部通过。已获得实际模拟 Submit、DELIVRD/UNDELIV、黑名单与频次拦截、退款、上行及 Webhook 503 后自动重试成功证据。 +- 真实发现 HTTP-0915-B03:正文“调测内容:https://qa0915.example.com”的识别结果是完整 URL,但 drainageTargets 向左扩展时把归一化后的中文冒号加入 URL,抛出 DRAINAGE_INVALID,已批准引流资料仍不能发送。此问题属于 URL 边界误判,不能把误拦截算作报备校验通过。 +- 修复目标:显式 http(s) URL 前的文字冒号不属于 URL;继续扩展恶意域名后缀、userinfo 和 query,保留已有安全校验。最终复测与发布状态后续追加。 + +- 定向回归:修复前 2 失败/20 通过,修复后 22/22 通过;候选全量门禁及部署后真实 URL 验收待执行。