Skip to content

fix(drivers/lanzou): support new /fn download page structure - #147

Open
Wudarensheng wants to merge 3 commits into
OpenListTeam:mainfrom
Wudarensheng:fix/lanzou-driver
Open

Wudarensheng wants to merge 3 commits into
OpenListTeam:mainfrom
Wudarensheng:fix/lanzou-driver

Conversation

@Wudarensheng

@Wudarensheng Wudarensheng commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

fix(drivers/lanzou): 适配蓝奏云 2026-10 新版 /fn 下载页

Branch: fix/lanzou-driver | Commit: 4cc1e2f | Base: main (82bd05d) | Diff: 4 files, +352 / −26

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

  • url(公开分享)模式:单文件分享恢复可用,能正常拿到文件名、大小、时间和下载直链。
  • cookie / account 模式不受影响(走 doupload.php 链路,本次未改动)。
  • 带提取码的分享分支行为不变(上游 Go 版同样未改动该分支)。

重要实现变化 / Implementation changes

  • 新增 parseFnPage():从 /fn 内页提取 apifile 绝对地址与签名表单;未命中改版结构时返回 null。
  • getFilesByShareUrl() 的无密码分支改为三级解析:旧版严格正则 → parseFnPage()(原样 POST 到
    绝对地址)→ 原有宽松兜底。旧版流程保持不变。
  • 新增私有 postShareAjax(),统一「POST → JSON 解析 → zt 校验」。
  • removeJSComment() 不再把 https:// 里的 // 当作行注释。改版页面大量使用绝对地址,原实现会
    把 src="https://..." 从 // 处截断,导致连 iframe 都取不到。
  • 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:

  • OpenList: 无需。同源修复已由上游 e1d88b0(#3157)合入,本 PR 是 Worker 侧移植。
  • OpenList-Docs: 无需。未新增配置项,驱动行为只是恢复可用。

Related Issues / 关联 Issue

Relates to OpenListTeam/OpenList#3157

Testing / 测试

  • go test ./... —— 不适用:本仓库是 TypeScript(Hono + Workers),Go 实现在上游仓库。
  • Manual test / 手动测试

自动化

npx tsx --test src/backend/drivers/lanzou/util.test.ts   # 6 pass / 0 fail
npx tsx scripts/test-lanzou.mts                          # 5 pass / 0 fail
npx tsc -p tsconfig.json --noEmit                        # lanzou 目录 0 报错
npm run build:edge                                       # ✓ Edge build complete

说明:

  • tsc 全量仍有 8 处既有报错(src/backend/drivers/quark_uc_tv/driver.test.ts 6 处、
    src/backend/internal/model/db_cipher.test.ts 2 处),与本 PR 无关,未一并处理。
  • npm run build 的前端拉取步骤在本机失败(git clone 触发 schannel
    CRYPT_E_NO_REVOCATION_CHECK,环境网络问题),因此只跑了 build:edge(TS 打包步骤)验证。

实机测试(生产环境,2026-10-22,本机直连)

分享 文件 结果
pan.lanzoui.com/b158157 → iYROC0ey8sj 追书神器3.138正式版换源.apk(21.4 M) 解析出 apifile…file=2425169;直链 Range 请求 HTTP 206,首字节 PK
pan.lanzoui.com/b174576 → 134136697 zip(306 B) 同上,HTTP 206

修复前后对照(同一张真实 /fn 页面):

修复前 修复后
旧版严格正则 不命中 不命中(按设计)
宽松正则 从绝对地址蒙对 file=2425169 不再使用该分支
实际 POST pan.lanzoui.com/ajaxfile.php?file=2425169 → HTTP 407(空 body) apifile.woozooo.com/ajaxfile.php?file=2425169 → HTTP 200、zt=1
表单 缺 websign,kd=0 完整签名,kd=1
结果 fetch failed,无直链 拿到 CDN 直链并可下载

Checklist / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING。
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。(待作者确认)
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    / 我已按适用情况使用 gofmt、go fmt 或 prettier 格式化变更代码。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者或代码所有者审查。(待作者操作)

关于格式化的说明: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 使用声明

  • This PR includes AI-assisted content.
    / 此 PR 包含 AI 辅助内容。

Tools used / 使用工具:

  • ChatGPT
  • Codex
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明): WorkBuddy AI

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-By attribution.
    / 我已确保所有 AI 辅助提交都包含 Co-Authored-By 归属信息。

  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。

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).
pikachuren

This comment was marked as outdated.

pikachuren

This comment was marked as outdated.

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 pikachuren left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 感谢 @Wudarensheng 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 GPT-6-Luna 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。项目维护者会综合考虑代码质量、项目规划、技术方向、团队资源等多方面因素做出是否合并的决策。

🎯 结论

🔄 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 外送风险,建议修复后再合并。

@pikachuren pikachuren left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 感谢贡献 PR #147!感谢 @Wudarensheng 提交~
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 DeepSeek-V4.1-Flash 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。项目维护者会综合考虑代码质量、项目规划、技术方向、团队资源等多方面因素做出是否合并的决策。

🎯 结论

🔄 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] 蓝奏云驱动可列目录但无法下载,报 ajaxfile.php 响应格式错误

2 participants