Repository navigation
fix(drivers/lanzou): support new /fn download page structure - #147
Wudarensheng wants to merge 3 commits into
Conversation
Lanzou moved the share download params into the /fn?<token> inner page, and the download API is now an absolute URL (https://apifile.woozooo.com/ajaxfile.php?file=N) that requires sign params (wp_sign / ajaxdata, action=downprocess, websign=2, kd=1, ves=1). The old relative-path regex ('/ajaxm.php?file=N') no longer matches, so parsing silently fell back to the wrong host with an incomplete form. - add parseFnPage() to extract the apifile URL and sign form, and use it when the legacy regex does not match (legacy flow kept untouched) - keep the previous lenient extraction as a last-resort fallback - fix removeJSComment() treating the '//' in https:// as a line comment, which truncated absolute URLs on the share page - use an absolute iframe src as-is instead of prefixing the share domain Adds unit tests for the new parser and the driver-level flow, and fixes the stale mock in scripts/test-lanzou.mts (it matched ajaxm.php while the driver posts to ajaxfile.php, so that case never actually ran). Ports OpenListTeam/OpenList@e1d88b0 (fix(drivers/lanzou): support new /fn download page structure, #3157).
Review follow-ups for the /fn page structure fix: - document parseFnPage()'s return contract: null means "not a 2026-10 /fn page", not an error, and the caller must fall back to the legacy parse - report an actionable error when the download page matches neither the legacy nor the /fn structure (previously "not find file id", which reads like a parse bug rather than "Lanzou changed again") - tests: parseFnPage returns null when wp_sign/ajaxdata are missing, legacy markers take priority on a page carrying both structures, unrecognized page yields the new error, and a driver-level legacy regression case so backward compatibility is asserted explicitly No behavior change for legacy pages.
pikachuren
left a comment
There was a problem hiding this comment.
🙏 感谢 @Wudarensheng 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 GPT-6-Luna 模型进行分析。
🎯 结论
🔄 Request Changes — 新版解析器将页面提供的任意 URL 直接用于服务端请求,且请求层可能附带驱动 Cookie,建议先加目标域校验并避免跨域转发凭据。
📖 概要
fix(drivers/lanzou): support new /fn download page structure · 适配蓝奏云 2026-10 /fn 新页面。核心改动:新增签名提取、绝对地址 POST 与旧结构回退,并补充解析及驱动级回归测试。
🧭 整体方案
实现采用“旧版严格匹配 → 新版 /fn 解析 → 宽松兜底”的兼容路线,覆盖了新旧页面变化,整体组织清楚;主要风险在于新版解析把远端页面中的接口地址作为可信目标使用。
📊 变更统计
4 个文件(+482 / -26 行) | 功能 ⭐⭐⭐⭐ | 最小改动 ⭐⭐⭐⭐ | 前向兼容 ⭐⭐⭐⭐⭐ | 方案设计 ⭐⭐⭐⭐
🚨 关键问题
P0(阻塞合并):无。
P1(建议修复后合并):
⚠️ src/backend/drivers/lanzou/util.ts:850:parseFnPage()从 HTTP 响应中提取domain1后直接交给postShareAjax(),而通用请求层在util.ts:448对该 URL 执行fetch(),没有校验协议或主机。若分享页/上游响应可被恶意控制,驱动会向任意外部或内网地址发起服务端请求;requestOnce()还会在util.ts:429-437附加驱动配置的 Cookie。是否考虑仅允许 HTTPS 且 host 命中明确的蓝奏云 API 域名白名单,并对非同源目标剥离 Cookie/认证头?建议补充拒绝127.0.0.1、私网地址及非白名单域名的测试。
P2(可选):无其他阻塞项。
📂 逐文件分析
src/backend/drivers/lanzou/util.ts
改动意图:提取新版 /fn 页中的 apifile URL 与签名表单,并保留旧页面兼容。
代码逻辑:旧版严格正则优先;未命中时调用 parseFnPage(),再回退到宽松解析。新增回退顺序合理,但新版 URL 由页面文本决定且直接进入通用 fetch,没有来源约束;这会扩大服务端请求的信任边界,并可能泄露请求层携带的 Cookie。建议在解析处使用 new URL() 校验 https: 与精确 host allowlist,并在通用请求层保证跨域时不转发凭据。
src/backend/drivers/lanzou/help.ts
修复 https:// 中的 // 被当作行注释截断的问题,修改与本次新版页面适配一致;未发现额外阻塞问题。
src/backend/drivers/lanzou/util.test.ts、scripts/test-lanzou.mts
新增新版解析、旧版回归、混合结构优先级和未知结构报错测试,覆盖了主流程。建议再增加恶意 domain1 的拒绝用例,验证 URL 白名单与敏感头隔离。
✅ 待处理清单
- [P1]
util.ts:850限制解析出的 URL 到受信任的 HTTPS 蓝奏云 API 域名,并禁止向其他 host 发送驱动 Cookie。 - 为非法 scheme、非白名单 host、loopback/私网目的地址添加回归测试。
- 若当前 CI 未执行
util.test.ts,将该测试接入 CI。
🎯 结论:🔄 Request Changes — 新版解析链路本身清晰,但信任页面中任意 URL 会引入 SSRF 与潜在 Cookie 外送风险,建议修复后再合并。
There was a problem hiding this comment.
🙏 感谢贡献 PR #147!感谢 @Wudarensheng 提交~
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 DeepSeek-V4.1-Flash 模型进行分析。
🎯 结论
🔄 Request Changes — 三级解析的回退设计很稳,但从页面里读出的绝对地址未经域名校验就发起 POST,建议补白名单
📖 概要
fix(drivers/lanzou): 适配蓝奏云 2026-10 新版 /fn 下载页 · 与上游 Go OpenList@e1d88b0 语义对齐
核心改动:新增 parseFnPage() 解析 apifile 绝对地址 + 签名表单;旧版严格正则 → /fn 解析 → 宽松兜底的优先级;修正 removeJSComment 把 https:// 当注释
🧭 整体方案
改版后「下载参数搬进 /fn?token 内页、接口换成 apifile 绝对地址 + 签名」,于是按「旧严格正则 → 新结构解析 → 历史宽松兜底」三级回退,旧结构行为保持不变。回退顺序与上游 Go 版一致,且保留了旧路径不动,兼容性考虑得很细致。另一个容易忽略的坑(removeJSComment 把 // 当注释)也一并修了,很到位。
📊 变更统计
4 个文件(+352 / -26) | 功能 ⭐⭐⭐⭐⭐ | 最小改动 ⭐⭐⭐⭐ | 前向兼容 ⭐⭐⭐⭐⭐ | 方案设计 ⭐⭐⭐⭐
🚨 关键问题
P1(建议修复):
⚠️ drivers/lanzou/util.ts的parseFnPage()从页面 HTML 里正则提取domain1(形如https://apifile.woozooo.com/ajaxfile.php?file=N),随后postShareAjax(fnPage.url, fnPage.form)原样 POST 到该绝对地址,中间没有任何域名/协议校验。也就是说「服务端请求的目标」由远端页面内容决定:一旦蓝奏云页面被篡改、或分享页里出现被注入的domain1,本站就会向任意主机发起带表单的 POST(SSRF / 内网探测 / 把签名参数外发)。
建议:加一个白名单校验,例如仅允许apifile.woozooo.com、.woozooo.com、.lanzou*.com(可复用已有的LANZOU_SHARE_DOMAINS再加 apifile 主域),不匹配时回退到旧版解析或直接报「页面结构不识别」。这既堵住 SSRF,也让"改版"变成显式失败而不是静默请求错误的主机。
P2(可选):
- 💡
drivers/lanzou/help.ts里removeJSComment用!result.endsWith(":")区分https://与行注释,能覆盖本次遇到的场景,但协议相对地址(src="//cdn.woozooo.com/x.js")的前两个字符仍是//且前面是引号,会被当行注释截掉。建议在同一处把「前一个字符是引号/等号/括号/空白」也算作非注释(或直接用小状态机跳过字符串字面量)。 - 💡 兜底分支里
fileIdMatch仍保留了cleanNextPage.match(/file=(\d+)/)这种宽泛正则——PR 描述里提到它会在 apifile 绝对地址上"蒙对"file ID 然后 POST 到错误主机。现在这条路径只在新旧结构都不匹配时才走,但它仍是"猜",建议考虑直接移除,改为抛出UNKNOWN_SHARE_PAGE_MSG(让"又改版了"显式暴露,而不是发出一个注定失败的请求)。
📂 逐文件分析
src/backend/drivers/lanzou/util.ts
改动意图:支持 apifile 绝对地址 + 签名表单的新下载链路。
代码逻辑:LEGACY_FILE_ID_RE(带引号的相对路径)命中 → 走原逻辑;否则 parseFnPage → 原样 POST 到绝对地址(无 referer,即回落 baseUrl);再否则走宽松兜底。
评价:postShareAjax 抽出「POST → JSON 解析 → zt 校验」消除了三段重复代码;iframe 绝对地址判断(/^https?:\/\//i)修掉了 https://hosthttps://host/fn?... 的拼接 bug;错误文案区分了「未识别结构」与「未找到文件 ID」,可维护性明显提升。唯一需要补的就是目标地址白名单。
src/backend/drivers/lanzou/help.ts
评价:注释里写清了「https:// 里的 // 不是注释」的由来,改动最小且不改其它分支行为。
src/backend/drivers/lanzou/util.test.ts / scripts/test-lanzou.mts
评价:removeJSComment、parseFnPage(命中/缺字段/旧版)、两种结构的 POST 目标与表单、iframe 绝对地址、混合页面优先级、未识别结构报错——7 条用例覆盖得相当完整。顺手修正了 scripts/test-lanzou.mts 里一直没生效的 mock(只匹配 ajaxm.php 而实际 POST 的是 ajaxfile.php),这个"用例从未真正跑过"的发现很有价值 👍
✅ 待处理清单
- [P1]
parseFnPage取到的绝对地址加域名/协议白名单校验 - [P2]
removeJSComment兼容协议相对地址(//cdn...) - [P2] 评估移除
file=(\d+)兜底猜测,改为显式报错
🎯 结论:🔄 Request Changes — 解析逻辑与兼容性都很好,只需补上"目标地址不可信"这一层校验。
Review follow-up (P1): parseFnPage() extracts the download API URL from the remote page text, and postShareAjax() POSTed to it verbatim. A tampered/injected page could therefore redirect the server-side request to any host (SSRF / intranet probing) and carry the driver cookie along. - add isTrustedLanzouApiUrl(): https only, hostname must belong to the lanzou host suffix allowlist (woozooo.com / lanzn.com / lanzouX.com family); rejects invalid URLs, non-https, loopback/private IPs, lookalike domains (evilwoozooo.com) and userinfo tricks (apifile.woozooo.com@evil.com) - reject untrusted URLs with an actionable error before issuing any request; the driver cookie can now only reach allowlisted lanzou hosts - drop the bare `file=(\d+)` / `file: N` guesses from the lenient fallback: they could "match" the file id inside an apifile URL and fire a doomed request to the wrong host; unrecognized pages now fail explicitly with UNKNOWN_SHARE_PAGE_MSG - removeJSComment(): also treat '//' immediately after a string-literal quote as URL content, so protocol-relative src="//cdn..." is no longer truncated - tests: allowlist unit cases, plus integration rejection without issuing any request for evil.example.com / 127.0.0.1 / 169.254.169.254 / http test:drivers 130 pass / 0 fail; lanzou util.test.ts 12 pass / 0 fail; scripts/test-lanzou.mts 6 pass / 0 fail
fix(drivers/lanzou): 适配蓝奏云 2026-10 新版 /fn 下载页
Summary / 摘要
蓝奏云在 2026-10 改版了单文件分享页:下载参数被移进
/fn?<token>内页,下载接口从相对路径'/ajaxm.php?file=N'变成绝对地址https://apifile.woozooo.com/ajaxfile.php?file=N,并且必须携带签名参数(页面里的
wp_sign/ajaxdata→action=downprocess、websign=2、kd=1、ves=1)。旧正则在改版页面上不再命中。更麻烦的是它不会直接报错:宽松正则会从 apifile 绝对地址里「蒙对」
file ID,然后 POST 到错误的主机、带着缺字段的表单,最终表现成
fetch failed。结果是 url(公开分享)模式下所有单文件分享都取不到直链。
用户可感知的行为变化 / User-visible changes
doupload.php链路,本次未改动)。重要实现变化 / Implementation changes
parseFnPage():从/fn内页提取 apifile 绝对地址与签名表单;未命中改版结构时返回null。getFilesByShareUrl()的无密码分支改为三级解析:旧版严格正则 →parseFnPage()(原样 POST 到绝对地址)→ 原有宽松兜底。旧版流程保持不变。
postShareAjax(),统一「POST → JSON 解析 →zt校验」。removeJSComment()不再把https://里的//当作行注释。改版页面大量使用绝对地址,原实现会把
src="https://..."从//处截断,导致连 iframe 都取不到。src已是绝对地址时直接使用,不再拼shareBaseDomain(原实现会拼成https://hosthttps://host/fn?...)。src/backend/drivers/lanzou/util.test.ts(6 例);修正scripts/test-lanzou.mts中一直失效的 mock(它只匹配
ajaxm.php,而驱动一直 POST 的是ajaxfile.php,所以那条用例从未真正跑过),并补一条改版结构的驱动级用例。
兼容性 / Compatibility
无破坏性变更:旧版页面结构仍走原分支,行为不变。
无配置项、存储格式、迁移或公开 API 变化。
与上游 Go 实现
OpenListTeam/OpenList@e1d88b0(#3157)语义对齐。This PR has breaking changes.
/ 此 PR 包含破坏性变更。
This PR changes public API, config, storage format, or migration behavior.
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
This PR requires corresponding changes in related repositories.
/ 此 PR 需要关联仓库同步修改。
Related repository PRs / 关联仓库 PR:
e1d88b0(#3157)合入,本 PR 是 Worker 侧移植。Related Issues / 关联 Issue
Relates to OpenListTeam/OpenList#3157
Testing / 测试
go test ./...—— 不适用:本仓库是 TypeScript(Hono + Workers),Go 实现在上游仓库。自动化
说明:
tsc全量仍有 8 处既有报错(src/backend/drivers/quark_uc_tv/driver.test.ts6 处、src/backend/internal/model/db_cipher.test.ts2 处),与本 PR 无关,未一并处理。npm run build的前端拉取步骤在本机失败(git clone触发 schannelCRYPT_E_NO_REVOCATION_CHECK,环境网络问题),因此只跑了build:edge(TS 打包步骤)验证。实机测试(生产环境,2026-10-22,本机直连)
pan.lanzoui.com/b158157→iYROC0ey8sjapifile…file=2425169;直链 Range 请求 HTTP 206,首字节PKpan.lanzoui.com/b174576→134136697修复前后对照(同一张真实
/fn页面):file=2425169pan.lanzoui.com/ajaxfile.php?file=2425169→ HTTP 407(空 body)apifile.woozooo.com/ajaxfile.php?file=2425169→ HTTP 200、zt=1websign,kd=0kd=1fetch failed,无直链Checklist / 检查清单
/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。(待作者确认)
gofmt,go fmt, orprettierwhere applicable./ 我已按适用情况使用
gofmt、go fmt或prettier格式化变更代码。/ 我已在适用情况下请求相关维护者或代码所有者审查。(待作者操作)
关于格式化的说明:
util.ts/help.ts在基线上本就不符合仓库 prettier 配置——.prettierrc只设了
semi: false,printWidth走默认 80,而既有代码按约 100 列书写。对整文件执行prettier --write会产生数百行与本 PR 无关的重排,因此新增代码只沿用文件既有风格,未整体重排;新增的
util.test.ts通过prettier --check。若维护者希望顺带统一格式,建议单独开一个style:PR。CI 目前没有格式校验。AI Disclosure / AI 使用声明
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
Usage scope / 使用范围:
Code generation / 代码生成
Refactoring / 重构
Documentation / 文档
Tests / 测试
Translation / 翻译
Review assistance / 审查辅助
I have reviewed and validated all AI-assisted content included in this PR.
/ 我已审核并验证此 PR 中的所有 AI 辅助内容。
I have ensured that all AI-assisted commits include
Co-Authored-Byattribution./ 我已确保所有 AI 辅助提交都包含
Co-Authored-By归属信息。I can reproduce all AI-assisted content included in this PR without any AI tools.
/ 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。