Skip to content

fix(mega): handle API Hashcash challenges - #166

Merged
PIKACHUIM merged 6 commits into
mainfrom
fix/issue-150-mega-hashcash
Nov 10, 2026
Merged

PIKACHUIM merged 6 commits into
mainfrom
fix/issue-150-mega-hashcash

Conversation

@PIKACHUIM

Copy link
Copy Markdown
Member

fix(mega): handle API Hashcash challenges

Branch: fix/issue-150-mega-hashcash
Commit: f2cdd18
PR creation link: https://github.com/OpenListTeam/OpenList-Worker/pull/new/fix/issue-150-mega-hashcash

Summary / 摘要

Fixes #150

Problem

The Mega driver reports Mega API HTTP error: 402 when MEGA returns a proof-of-work challenge. The linked megajs issue documents the protocol change: an API response carries an X-Hashcash challenge, and a valid proof must be sent in that header when retrying the request.

Changes

  • Parse and validate version-1 Hashcash challenges, including the 48-byte token.
  • Generate the official four-byte nonce proof with WebCrypto SHA-256 and the repeated-token payload.
  • Retry a challenged request once with the official X-Hashcash header; keep ordinary 402 responses without a challenge as errors.
  • Bound proof-of-work to 4,096 attempts and 10 seconds to prevent unbounded work in serverless runtimes.
  • Add tests for challenge parsing, nonce progression, header format, retry behavior, malformed challenges, ordinary 402 responses, and the work budget.

Verification

  • tsx --test src/backend/drivers/mega/util.test.ts src/backend/drivers/mega/driver.test.ts — 8/8 passed.
  • npm run test:drivers — 205/205 passed.
  • npm run test:server — 208/208 passed.
  • npm run lint — passed.
  • Mutation check: unconditional/repeated challenges are rejected by the budget/retry guards.
  • A local Node WebCrypto solve for easiness 192 completed in 133 ms; this is not a Cloudflare Workers benchmark.
  • No live MEGA account or production API request was used.
  • 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: N/A
  • OpenList-Docs: N/A

Related Issues / 关联 Issue

Testing / 测试

  • go test ./... (not applicable: OpenList-Worker is TypeScript)
  • tsx --test src/backend/drivers/mega/util.test.ts src/backend/drivers/mega/driver.test.ts — 8/8 passed.
  • npm run test:drivers — 205/205 passed.
  • npm run test:server — 208/208 passed.
  • npm run lint — passed.
  • Manual test / 手动测试: no live MEGA credentials/service request was used.

Checklist / 检查清单

  • I have read [CONTRIBUTING](https://github.com/OpenListTeam/OpenList-Worker/blob/main/CONTRIBUTING.md).
    / 已阅读贡献指南。
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。
  • Changed TypeScript was formatted and git diff --check passed.
    / 已格式化修改的 TypeScript,且 git diff --check 通过。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者或代码所有者审查。

AI Disclosure / AI 使用声明

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

Tools used / 使用工具:

  • Other: WorkBuddy (GPT-6-Luna)

Usage scope / 使用范围:

  • Code generation / 代码生成
  • Tests / 测试
  • Documentation / 文档
  • Review assistance / 审查辅助
  • The AI-assisted code and tests have been reviewed and validated.
    / AI 辅助代码和测试已审查并验证。
  • All AI-assisted commits include a Co-Authored-By trailer.
    / 所有 AI 辅助提交都包含 Co-Authored-By 归属信息。
  • I can reproduce all AI-assisted content without AI tools.
    / 我可以在没有 AI 工具的情况下重现所有 AI 辅助内容。

Note: the pushed commit currently has no Co-Authored-By trailer. Add a verified at

Issue #150 reports HTTP 402 from the MEGA API. The linked megajs issue identifies the cause as a 402 response carrying X-Hashcash, which the official MEGA web client solves and retries. The Worker client previously treated every non-2xx response as a terminal error.

Parse version-1 Hashcash challenges, validate the 48-byte token, and compute the 4-byte little-endian nonce using the official repeated-token SHA-256 input and threshold. Retry a challenged request once with the official X-Hashcash header; preserve ordinary 402 errors when no challenge is present, and cap attempts/time to avoid unbounded serverless CPU work. Add tests for nonce/threshold, malformed challenges, API retries, work limits, and non-Hashcash 402 responses.

Fixes #150

Validation: Mega tests 8/8; npm run lint passed; a real WebCrypto solve for easiness 192 completed in 133 ms on the local Node runtime; git diff --check passed. No live MEGA credentials/service request was used.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
openlist-work 21e70ff Nov 10 2026, 12:57 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
openlist-tsworkers 21e70ff Nov 10 2026, 12:45 PM

@PIKACHUIM PIKACHUIM mentioned this pull request Nov 2, 2026
4 tasks done

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

🎯 结论

🔄 Request Changes — 算法实现与测试覆盖都没问题,但想先确认「服务端 CPU 预算」这条硬约束

📖 概要

fix(mega): handle API Hashcash challenges · Fixes #150 · 修复 MEGA 返回 402 工作量证明挑战时的报错
核心改动:解析 X-Hashcash 挑战、用 WebCrypto 求解 4 字节 nonce、带 X-Hashcash 重试一次并限制重试次数

🧭 整体方案

按 megajs 的协议实现补上 Hashcash 分支,把「求解器」抽成可注入依赖(默认 generateHashcashToken)以便测试,同时用 attempts + 墙钟双预算兜住 serverless 的无界计算风险。分层清晰,测试注入点设计得很好。

📊 变更统计

2 个文件(+363 / -7) | 功能 ⭐⭐⭐⭐⭐ | 最小改动 ⭐⭐⭐⭐ | 前向兼容 ⭐⭐⭐⭐⭐ | 方案设计 ⭐⭐⭐⭐

🚨 关键问题

P1(建议先确认):

  • ⚠️ drivers/mega/util.ts:216-218 的工作量量级:HASHCASH_BUFFER_BYTES = 4 + 48 * 262144 ≈ 12.0 MiB,每一次尝试都要对这 12MB 做一次 SHA-256;easiness=192 时 threshold = 1 << 24,命中概率 ≈ 1/256,期望尝试约 256 次 → 总哈希量约 3GB。PR 描述里「本地 Node WebCrypto 求解 easiness=192 用了 133ms」与这个量级对不上(12MB × N 次),想确认一下这个 benchmark 的口径(是否用了 mock digest / 极小 token / 只测了单次 digest?)。
    建议:
    1. 在 PR 里补一条真实 challenge 的实测数据(CF Workers 上 256 次尝试的实际 CPU 时间;免费版 CPU 上限 10ms、付费版 30s,ESA 也有单请求时限);
    2. 把结论写进驱动说明(例如「MEGA 上传/列目录在 Worker 免费版可能超 CPU」),或让预算与运行时对齐(如默认 maxAttempts 按 easiness 估算的期望尝试数 × 2 收敛,而不是固定 4096);
    3. 超预算时的错误文案已可读,这点很好 👍

P2(可选):

  • 💡 drivers/mega/util.ts:407 const detail = await res.text().catch(() => "") 未截断,上游若返回整页 HTML,会把全文塞进错误信息与日志。建议 slice(0, 512)。
  • 💡 测试 util.test.ts 里 TOKEN = "A".repeat(64)(48 个 0 字节)与生产中真实 token 的重复结构一致,用例有效;但建议补一个「challenge 里 token 含 -/_(base64url 变体)」的用例,decodeHashcashToken 的正则同时放行了 +/ 和 -_,值得断言一次。

📂 逐文件分析

src/backend/drivers/mega/util.ts

改动意图:解决 MEGA API 402 + X-Hashcash 挑战下的请求失败。
代码逻辑:小端累加 4 字节前缀 → 对「前缀 + token 重复 262144 次」做 SHA-256 → 取摘要首 32 位大端与 ((easiness&63)<<1)+1) * 2^((easiness>>6)*7+3) 比较 → 命中则把前缀 base64url 化作为 proof。
问题分析:算法与官方实现一致(base * 2**shifts 在 shifts ≤ 24 时等价于官方 base << shifts,无溢出),重试上限 MAX_HASHCASH_RETRIES = 1 与「重复挑战即报错」的守卫正确,res.body?.cancel() 也避免了响应体泄漏。唯一的实质风险就是上面的 CPU 预算。
详细建议:即使不改默认值,也建议在 getSearchIndexConfig 之外的地方(驱动文档/注释)明确「一次求解的期望开销 ≈ 12MB × 256 次 SHA-256」,让运维能判断自己的套餐是否够用。

src/backend/drivers/mega/util.test.ts

评价:挑战解析、nonce 递增、header 格式、重试、普通 402、预算耗尽、重复挑战均有覆盖,且用 digest 注入精确控制了「先失败后成功」,比打桩 fetch 更可靠。

✅ 待处理清单

  • [P1] 补充 CF Workers/ESA 上的真实求解耗时,确认默认预算与套餐 CPU 上限匹配
  • [P2] util.ts:407 错误详情截断(≤512 字符)
  • [P2] 补 base64url 变体 token 的单测用例

🎯 结论:🔄 Request Changes — 实现正确,但「12MB × N 次 SHA-256」在 serverless 上是否可行需要先用实测数据背书。

固定 4096 次尝试在 easiness=192(期望约 256 次)时意味着 16 倍无效哈希,而每次尝试都要对 ~12 MiB 缓冲做一次 SHA-256(本地 WebCrypto 实测单次约 7.5 ms,512 次约 3.9 s)。默认预算改为 min(期望次数 x 2, 4096),保留 10 s 墙钟上限,并在注释里写明平台 CPU 代价。另:上游错误正文截断到 512 字符,避免整页 HTML 进日志;补 base64url token、预算收敛与错误截断三条回归用例。
pikachuren

This comment was marked as outdated.

2 倍期望只有 86.5% 成功率(easiness=192 时 512 次),为省尾部开销把失败率抬到 13.5% 不划算;改为 4 倍(1024 次、98.2%、约 7.7 s),仍落在 10 s 墙钟内。另修 P2:res.text().slice(0,512) 只截断输出,正文仍被完整缓冲;改为流式读取前 512 字节后 cancel,并补按 chunk 计数的回归用例。

@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 #166!感谢 @PIKACHUIM 提交~
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 DeepSeek-V4.1-Flash 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。

🔄 增量评审:219d5fd → a174e7d

本轮新增 1 个 commit,改动文件:drivers/mega/util.ts、drivers/mega/util.test.ts(+66 / -12)

新增改动的问题:

  • 💡 [P2] util.ts 的 readTextPrefix 把 res.body?.getReader() 放在了 try 外面,而调用处 const detail = await readTextPrefix(res, 512) 也没有 .catch。上一版是 (await res.text().catch(() => ""))——无论读正文出什么错,都保证能抛出带状态码的 Mega API HTTP error: 500。现在如果 body 已被消费/锁定(getReader() 抛 TypeError)或 TextDecoder 抛错,拿到的是原始 TypeError 而不是那条有意义的 HTTP 错误,排查时反而丢了状态码。
    建议改为 const detail = await readTextPrefix(res, 512).catch(() => ""),保持与旧行为一致的兜底。
    顺带一提:if (!value || value.length === 0) continue 这个分支没有任何推进保护,若某个流持续返回空 chunk 会空转。实际 undici 不会这么干,把空 chunk 也当 done 处理会更稳。

  • 💡 [P3] util.test.ts:219 注释里的成功率算错了。easiness=255 → threshold = 0x7f000000 = 2130706432 → threshold+1 = 2130706433,真实命中率 p ≈ 0.4961,12 次成功率约 99.97%;即便退一步按 E=3 的保守口径 1-(1-1/3)^12 算也是 99.23%,不是注释里的 98.8%(98.8% 对应的是 11 次)。建议改成 99.2%(保守口径),免得以后有人把这个数字当基准去调倍数。

旧问题解决情况:

  • ✅ [P1] 预算倍数 2× → 4× → 新增 HASHCASH_BUDGET_MULTIPLIER = 4 并把取舍写进注释(1-(1-1/E)^(4E) ≈ 98.2%,easiness=192 时 1024 次 × 7.5ms ≈ 7.7s 仍在 10s 墙钟内,并明确否定 2× 的 86.5%)。正好落在我上轮建议的 4× 下限,常量 + 注释 + 用例三处口径一致。
  • ✅ 预算收敛用例同步更新 → calls 从 6 改为 12,与 easiness=255 → 期望 3 → 3×4=12 对得上,没有出现"改了实现忘了改断言"的常见疏漏。
  • ✅ [P2] res.text().slice(0, 512) 改为流式读取 → readTextPrefix() 按 reader.read() 累积到 512 字节即 reader.cancel(),不再完整缓冲整页;subarray 处理了 chunk 跨越上限的截断,finally 里 cancel() 也包了 try/catch。
  • ✅ P2 的用例改得更严 → 用例从"4000 字符 HTML"换成 200 × 64KiB 的 ReadableStream,并新增 assert.ok(pulled <= 2)。这条断言比原来的 message.length < 600 有价值得多:改回 res.text() 会拉满 200 个 chunk,断言直接失败;而长度断言在两种实现下都能通过。原有的长度断言也保留了,属于双保险。

🎯 结论:✅ Approve — 上轮的 P1(预算倍数)与 P2(错误正文缓冲)都已按建议修到位,且用例比原来更严格;仅剩一处注释数字笔误和一个 .catch 兜底的小建议。

pikachuren added 3 commits November 9, 2026 18:19
#150)

MEGA 的 402 只是反爬层的 Hashcash 挑战,PR #166 已能正确求解;挑战通过后
服务端返回 -9(ENOENT),因为驱动从未实现登录协议:uh 直接用了
base64url(email),us 响应里也没有可用的 sid。

- us0 预登录识别 v1/v2,v1 走 prepareKey(65536 轮 AES-128-ECB) + stringhash,
  v2 走 PBKDF2-SHA512(100000) 并取派生值后 16 字节作为 uh
- us 登录携带可选 mfa;解 k 得到 master key,解 privk 后 RSA 解 csid 得到 sid
- 用 master key 解节点密钥,再用节点密钥 AES-CBC 解出属性/文件名
- getChildren 排除根节点自身;建目录时生成随机密钥并加密属性与密钥
- 新增 crypto.ts(AES-128、prepareKey、stringhash、MPI/RSA)及对照 node:crypto
  的测试,登录流程用合成账号覆盖 v1/v2/2FA/-9
MEGA 的 PoW 只出现在 402 + X-Hashcash;503 "Server Too Busy" 是网关过载/
限流,与 PoW 无关,官方客户端与 megajs 都是退避后重试。此前驱动把 503
直接抛成 "Mega API HTTP error: 503",列目录/登录偶发失败。

- 503/500/502/504/429 与 JSON -3(EAGAIN)/-4(ERATELIMIT) 做指数退避重试,
  最多 4 次(400ms 起,封顶 8s,带抖动,尊重 Retry-After)
- 单元素数组形式的错误码(如 [-9])也会被识别,统一抛 MegaApiError,
  登录侧继续翻译 -9/-16/-26
- 已解出的 Hashcash proof 在随后的 503 重试中复用,不重复求解
- 新增 4 条测试:503 重试、重试上限、-3/-4、402→503→200 保留 proof
每次 isolate 冷启动都会重新登录 MEGA,而登录会触发一次 Hashcash PoW
(期望 256 次、每次 12 MiB SHA-256,约 1-2 秒)。把登录态持久化后,
冷启动只需一次 fetchNodes。

- 登录成功后把 sid + master_key 写回 storage.addition(master key 是
  解密节点密钥/属性必需的,缺它只能拿到加密文件名)
- 驱动启动时优先复用会话;遇到 -15(ESID) 或会话格式损坏会回退重新登录
- 持久化复用 139/GuangYaPan 的 deferredTokenPersistence 逻辑,避免
  /admin/storage/update 用旧快照覆盖刚写入的会话
- prepareKey 的 65536 轮都使用同一把密钥,改为只做一次密钥扩展并原地
  加密,本地实测 83ms → 55ms;试过 WebCrypto 逐块 encrypt,65536 次
  subtle.encrypt 需要 ~1.15s,更慢,故保留同步纯 JS
- 新增 6 条测试:会话复用、过期重登、无缓存登录、异常传播、持久化失败
  容错、损坏会话回退,以及客户端 restoreSession/clearSession
@PIKACHUIM
PIKACHUIM merged commit e6eb21f into main Nov 10, 2026
3 checks passed
WMZZwmzz added a commit to WMZZwmzz/OpenList-Worker that referenced this pull request Nov 14, 2026
合并 OpenListTeam/OpenList-Worker 上游 main(82bd05d..e6eb21f,21 个提交),
保留本仓库全部安全加固与本地特性:

上游引入:
- feat(media): 媒体库 / Emby / Subsonic 完整对齐(OpenListTeam#148),路由已挂载
- feat(mega): Hashcash 工作量证明 + 会话持久化(OpenListTeam#166/OpenListTeam#152 相关)——移植
  generateHashcashToken 至本地 mega 实现,保留本地流式下载与纯 JS 密码学
- feat(config): wrangler keep_vars(OpenListTeam#176);alias iconv-lite 空桩(OpenListTeam#160)
- fix(webdav): 下载重定向签名(OpenListTeam#187/OpenListTeam#175),统一走 appendDownloadSignToRawUrl
- fix(auth): guest 默认禁用(OpenListTeam#162/OpenListTeam#133);2FA QR SVG(OpenListTeam#153,本地保留自研 PNG 实现)
- fix(139): SMS 验证码登录状态回调(OpenListTeam#190/OpenListTeam#164)——本地 singleflight 与
  still-valid 降级保留,叠加 onLoginStateChange 持久化
- refactor(cache)!: 移除持久缓存与 singleflight(OpenListTeam#185),本地 cache 审计修复
  (F-104/F-106)随之失效但安全语义由无缓存架构天然保证
- fix(115): 移除损坏的 legacy 115 驱动(OpenListTeam#152),旧别名统一路由 Pan115Driver
- refactor(driver): put() 内容契约统一 Uint8Array(OpenListTeam#173/OpenListTeam#169)

本地保留(关键项):
- 驱动注册表架构(F-271/F-090 精确匹配)+ 全部 25 处刷新令牌持久化回调
- pkg/totp 自研 QR、pkg/upload-limit 流式上限判定(F-057)、pkg/sign
  专用下载签名密钥 + canonicalVirtualPath + scope 支持
- op/storage F-039 setup 锁内重读、F-005 挂载点复查;sql 格式 90 参数
  预算分批(F-083,上游 OpenListTeam#184 同源修复);readBodyWithinLimit 流式读取
- 139 getDisk 分页/encodeURI 方向/CSPRNG、115open parentPath、
  mega TOTP/流式下载、weiyun F-166 无哈希上传、ftp F-262 时序
- DB_CIPHER 默认 aes-256-gcm、EdgeOne frozen-lockfile、OpenList Moe 主题
- getDriver deferTokenPersistence 修复为 return await(延迟令牌持久化
  需覆盖驱动 init 全程;此隐患 base/上游均存在)

验证:tsc 0 错误;build 通过(Moe 主题注入正常);test:all 8 套
1366 用例全部通过。
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] HTTP 402 for Mega

2 participants