Repository navigation
fix(mega): handle API Hashcash challenges - #166
Conversation
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.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
openlist-work | 21e70ff | Nov 10 2026, 12:57 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-tsworkers | 21e70ff | Nov 10 2026, 12:45 PM |
There was a problem hiding this comment.
🙏 感谢贡献 PR #166!感谢 @PIKACHUIM 提交~
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 DeepSeek-V4.1-Flash 模型进行分析。
🎯 结论
🔄 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?)。
建议:- 在 PR 里补一条真实 challenge 的实测数据(CF Workers 上 256 次尝试的实际 CPU 时间;免费版 CPU 上限 10ms、付费版 30s,ESA 也有单请求时限);
- 把结论写进驱动说明(例如「MEGA 上传/列目录在 Worker 免费版可能超 CPU」),或让预算与运行时对齐(如默认
maxAttempts按 easiness 估算的期望尝试数 × 2 收敛,而不是固定 4096); - 超预算时的错误文案已可读,这点很好 👍
P2(可选):
- 💡
drivers/mega/util.ts:407const 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、预算收敛与错误截断三条回归用例。
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
left a comment
There was a problem hiding this comment.
🙏 感谢贡献 PR #166!感谢 @PIKACHUIM 提交~
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 DeepSeek-V4.1-Flash 模型进行分析。
🔄 增量评审: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 兜底的小建议。
#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
合并 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 用例全部通过。
fix(mega): handle API Hashcash challenges
Branch:
fix/issue-150-mega-hashcashCommit:
f2cdd18PR 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: 402when MEGA returns a proof-of-work challenge. The linked megajs issue documents the protocol change: an API response carries anX-Hashcashchallenge, and a valid proof must be sent in that header when retrying the request.Changes
X-Hashcashheader; keep ordinary 402 responses without a challenge as errors.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./ 此 PR 包含破坏性变更。
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
/ 此 PR 需要关联仓库同步修改。
Related repository PRs / 关联仓库 PR:
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.Checklist / 检查清单
/ 已阅读贡献指南。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
git diff --checkpassed./ 已格式化修改的 TypeScript,且
git diff --check通过。/ 我已在适用情况下请求相关维护者或代码所有者审查。
AI Disclosure / AI 使用声明
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
Usage scope / 使用范围:
/ AI 辅助代码和测试已审查并验证。
Co-Authored-Bytrailer./ 所有 AI 辅助提交都包含
Co-Authored-By归属信息。/ 我可以在没有 AI 工具的情况下重现所有 AI 辅助内容。
Note: the pushed commit currently has no
Co-Authored-Bytrailer. Add a verified at