fix(sign): /fs/get raw_url 缺少下载签名导致下载 401 (#66) - #70
Merged
Merged
Conversation
预览页的下载按钮直接使用 /api/fs/get 返回的 raw_url(<a href={raw_url}>),图片/视频预览也直接把它当 src,前端不会再自己拼 ?sign=。而 raw_url 指向需要验签的 /p 端点,sign_all / 存储级 enable_sign / 密码 meta 任一命中时缺签名必然 401 sign verify failed:表现为预览页正常、点下载就 401(.exe 这类无内容预览的文件尤其明显)。
对齐 Go handles.FsGet:需要签名时把 ?sign= 追加到 raw_url。同时把 raw.ts buildDownProxyUrl 的补签条件从只看 sign_all 改为 needDownloadSign,与验签侧一致,避免只靠 enable_sign/密码 meta 的部署被 302 到无签名的地址。
新增回归测试 fs_get_rawurl_sign.test.ts:真实 Local 存储 + sign_all,校验 raw_url 带签名、签名可通过验签、直接请求 raw_url 不再 401,并附篡改签名必 401 的对照。
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
路径编码:新增 pkg/path.encodeDownloadPath,对齐 Go utils.EncodePath(path, true) (保留 A-Za-z0-9-._~ 与 $&+,:;=@,其余含 % / ? / # / 空格 / 非 ASCII 逐字节编码)。 raw_url(getItem)、down_proxy_url 模板、WebDAV 302 Location、seed 自取地址统一改用 该函数。修复:文件名含 % 时 /p 的 decodeURIComponent 抛 URIError 导致 500(现回 400); 含 ? / # 的链接会被浏览器当成 query / fragment 截断成另一个路径。 link_expiration:单位由秒改为小时、0 表示永不过期(对齐 Go internal/sign 的 time.Duration(expire)*time.Hour 与官方文档 configuration/global.md "in hours")。 签名 expire=0 即永久,验签跳过超期判定(对齐 Go pkg/sign 的 expires != 0 分支); 负数有效期仍立即过期。 down_proxy_url:补签条件对齐 Go GenerateDownProxyURL —— 只看 disable_proxy_sign, 不再要求与请求同 host,避免 CDN / 前置代理 / 同实例另一域名这类典型配置拿到无签名 地址而必然 401;并修正「Go 支持 $path 占位符」的错误注释(Go 只是拼接 EncodePath)。 getDownProxyUrl 多行只取第一行(对齐 strings.Split(..., "\n")[0])。 /fs/link 兜底分支对齐 Go handles.Link:路径编码 + 无条件携带签名(Go 不看 needSign), 否则 sign_all / enable_sign / 密码 meta 生效时复制出来的链接必然 401。 新增 src/backend/server/sign_go_parity.test.ts:路径编码字符集合、含 % / # / 中文 的 raw_url 端到端可用、非法转义回 400、link_expiration 小时换算、expire=0 永久可验、 负有效期即过期。
4 tasks
4 tasks
按 .github/PULL_REQUEST_TEMPLATE.md 撰写 docs/pr-sign-raw-url.md:根因链路(前端把 /fs/get 的 raw_url 当下载地址、服务端从未补签)、与 #51 的区分表、Go 对齐清单与有意 保留的差异、两处配置语义变更(link_expiration 单位与 0 语义)的兼容性提示、测试与 反证结果、以及未一并修复的同类出口。 顺带修正 storage.ts 中指向 pkg/utils 的过期注释(编码函数已移至 pkg/path.ts)。
PR 正文属评审材料,不进入仓库(本工作区既有约定是放在仓库外的 PR-*.md)。 内容已移至 ../PR-fix-66-sign-raw-url.md,可直接粘贴为 GitHub PR 正文。
#66 的补签修复上线后暴露第二个问题:预览页「下载」返回 403 proxy not allowed, 而文件列表右键下载正常。 原因:raw_url 恒为 `/api/p`,但 `/p` 会先跑 Go 的 canProxy() (MustProxy || WebProxy || webdav_policy=use_proxy_url || proxy_types || text_types),不通过直接 403;`/d` 不受该门禁限制。文件列表走 `/d` 所以正常, 预览页的「下载」按钮与 img/video 的 src 用的却是 raw_url。 - op/storage.ts:新增 resolveRawUrlPrefix(),用 canUseProxyEndpoint() 决定 raw_url 用 /api/p 还是 /api/d(与 Go handles.FsGet 只在 MustProxy||WebProxy 时才给 /p、 其余走直链的语义一致,等价地落在 /d 上由 rawRouter 决策 302/proxy)。 - server/webdav.ts:删除重复的 prefix 计算(其 rawUrl 分支早已恒真,逻辑是死代码), 直接用 getItem 的结果;未再使用的 import 一并移除。 - server/seed.ts:直链优先,回退用 getItem 的代理地址(前缀与编码已定),不再硬编码 /api/p,否则未开代理的存储取种子文件同样 403。 - 新增 server/raw_url_prefix.test.ts:前缀选择(非代理→/d、web_proxy→/p)、 proxy_types/text_types 判定、以及 /p 403 与 /d 非 403 的 HTTP 层对照。
实机报告的两个问题:
1. 文件预览页刷新报 Static resource not found(实测 404)
assets.ts 里的「CDN 静态资源重定向」路由写成了 `/:folder/:filepath*`,
它会吞掉**任意两段路径**。而文件预览页的地址正是 `<域名>/<挂载点>/<文件>`
(例如 /存储2/xxx.exe),刷新时被它拦下,未配置 ASSET_URLS 时直接返回
404 `Static resource not found`,请求永远走不到 SPA 兜底。
改为对齐 Go server/static/static.go:只对固定目录白名单
(assets / images / streamer / static)做 CDN 重定向;未配置 ASSET_URLS 时
调用 next() 放行给本地静态资源(Workers ASSETS 绑定)与 SPA 兜底,
不再回 404。logo / favicon 的重定向保持不变。
2. 后台存储编辑页的「序号」(storage.order)设置后排序不变
- internal/op/storage.ts:虚拟根目录合并挂载点时直接沿用数据库数组顺序,
现按 Go op.getStorageVirtualFilesByPath 的比较器排序:Order 升序,
Order 相同时按 MountPath 升序;
- server/admin.ts:/storage/list 按 Go db.GetStorages(addStorageOrder =
`order, id`)排序返回,此前是数组原始顺序。
新增测试:
- server/assets_route.test.ts(5 例):`<挂载点>/<文件>` 刷新落到 SPA 兜底、
未配置 ASSET_URLS 时 /assets/* 不再 404、配置后仅四个目录 302 且 $version
被替换、logo/favicon 仍重定向;
- server/storage_order.test.ts(4 例):根目录挂载顺序(order / mount_path /
跳过 disabled)、后台存储列表 order,id 顺序。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(sign): 修复 /fs/get raw_url 缺少下载签名导致的 401,并对齐 Go 的路径编码与过期语义Summary / 摘要
本 PR 修复 #66
[BUG] sign verify failed:下载始终 401,而预览看起来正常。根因:
/api/fs/get返回的raw_url形如/api/p/<path>,指向的正是需要验签的/p端点,但从来不携带?sign=;而前端把raw_url原样当作下载地址使用,不会再自己拼签名:于是只要
sign_all/ 存储级enable_sign/ 密码 meta 任一命中(即needDownloadSign() === true),从预览页点「下载」就必然得到401 sign verify failed。issue 里贴出的请求GET https://xxx/api/p/存储1/7z2602-x64.exe完全没有?sign=,与该链路完全吻合(sign_all在 Go 版里默认就是true,导入 Go 备份的部署必然踩中)。Go 版是显式补签的:
第二个提交把该链路周边与 Go 不一致的既有实现一并对齐:下载路径编码、
link_expiration的单位与 0 语义、down_proxy_url的补签条件、/fs/link的签名。用户可感知的行为变化
sign_all/enable_sign/ 密码 meta 生效时,预览页的「下载」按钮、图片/视频/PDF 预览、S3 网关与 WebDAV 的 302 目标不再返回401 sign verify failed。%时/p、/d下载不再 500(此前decodeURIComponent抛URIError;现在返回400 Bad Request: malformed path encoding)。#/?/ 空格的下载链接不再被浏览器当成 fragment / query 截断成另一个路径。link_expiration的单位由「秒」改为 小时(与 Go 及官方文档一致),且0表示永不过期(不再回退到「24 小时」)。down_proxy_url的补签不再要求目标与请求同 host,只要未开启disable_proxy_sign就会带上签名(对齐 GoGenerateDownProxyURL)。/fs/link的兜底分支改为「路径编码 + 无条件携带签名」(对齐 Gohandles.Link)。重要实现变化
server/fs.ts:/fs/get的raw_url按需补签,条件与验签侧needDownloadSign()完全对称;/fs/link兜底分支补签 + 编码。pkg/path.ts(新增):encodeDownloadPath(),逐段对齐 Goutils.EncodePath(path, true)。internal/op/storage.ts:getItem()生成的rawUrl改走encodeDownloadPath()(单点覆盖/fs/get、S3、WebDAV、seed 的取址)。pkg/sign.ts:link_expiration按小时换算;expires === 0表示永不过期(签发与验签两侧都实现,对齐 Gopkg/sign的expires != 0判定)。server/raw.ts:down_proxy_url模板改用同一编码、补签条件对齐 Go;非法百分号转义回 400 而不是 500;修正「Go 支持$path占位符」的错误注释(Go 只是拼接EncodePath)。internal/driver/proxy.ts:getDownProxyUrl()多行只取第一行(对齐 Gostrings.Split(..., "\n")[0])。server/webdav.ts/server/seed.ts:/api/p拼接改走同一编码函数。配置 / 存储 / API 变化
无数据库 schema 变更、无新增配置项。
link_expiration的取值单位语义发生变化(秒 → 小时),且0= 永不过期。详见下方兼容性说明。/fs/get的raw_url在需要签名时会多一个?sign=查询参数(签名本身不会二次拼:已带sign的 URL 原样返回)。新增 11 个测试用例(
server/sign_go_parity.test.ts7 个 +server/fs_get_rawurl_sign.test.ts4 个)。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:
link_expiration的单位在官方文档中已写明为小时)Related Issues / 关联 Issue
Fixes #66
Testing / 测试
go test ./...(本项目为 TypeScript,不适用;替代命令见下)本项目使用的等价命令:
结果是确定性的:
src/backend/server/*.test.ts失败的 4 例是既有问题,与本 PR 无关——用git stash暂存本 PR 的两个提交后跑同一套命令,失败集合完全一致(init_setup_guard/init_setup_blob的 3 个初始化用例 +CAS codec matches casmeta base64 JSON field names)。这些用例单独运行时全部通过,属进程内的既有耦合问题。反证(证明回归测试真的盯住了本 bug):把
fs.ts的修复临时改回raw_url: rawUrl,新测试立刻失败并打印出与 issue 完全同形的地址:新增测试覆盖:
server/fs_get_rawurl_sign.test.ts(4 例):真实 Local 存储 +sign_all,走/api/fs/get断言raw_url带签名、sign字段与 URL 内一致、verifyDownloadSign通过、直接请求该raw_url不再 401;另加「篡改签名必 401」对照与「无需签名时不追加sign」。server/sign_go_parity.test.ts(7 例):EncodePath(path, true)一致(100%.txt → 100%25.txt、a?b.txt → a%3Fb.txt、a#b.txt → a%23b.txt、a b.txt → a%20b.txt、存储1 → %E5%AD%98%E5%82%A81,$&+,:;=@保留);%/#/ 中文 的文件名端到端可用(/fs/get→ 直接请求raw_url非 401、非 500);/p对非法百分号转义返回 400;link_expiration = 24→ 有效期86400秒(*-time.Hour);sign_all+link_expiration = 0→ 签名expire字段为0且可验签(GoNotExpired);Checklist / 检查清单
/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
gofmt,go fmt, orprettierwhere applicable./ 我已按适用情况使用
gofmt、go fmt或prettier格式化变更代码。/ 我已在适用情况下请求相关维护者或代码所有者审查。
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 辅助内容。
Implementation Notes / 实现说明
为什么会「预览正常、下载 401」
signuseLink()组装的/d、/p链接/fs/list返回的sign)<a href={objStore.raw_url}><img>/<video>/pdf.js的src={objStore.raw_url}issue 报告者用的是
.exe——这类文件没有内容预览,预览页只渲染元信息 + 下载按钮,所以「预览正常」而「下载 401」。问题比报告的更普遍:任何有内容预览的文件在同样配置下连预览都会坏。这也解释了为什么仓库用例signed_link_auth.test.ts一直没覆盖到——它只测了/p/*端点本身,没测「服务端返回给前端的 URL」是否合法。与 Go 的对齐清单
已对齐(本 PR 修复):
raw_url补签isEncrypt(meta, path) || SignAllsignPolicy.enabled || isEncryptPath()(同一判定,needDownloadSign()与验签侧共用){apiUrl}/p{EncodePath(path, true)}{query}/api/p{encodeDownloadPath(path)}{query}utils.EncodePath(path, true)pkg/path.encodeDownloadPath()(字符集合一致)link_expiration单位 / 0NotExpiredexpire === 0且验签跳过超期判定down_proxy_url补签disable_proxy_signdisable_proxy_sign(不再要求同 host)getDownProxyUrl多行strings.Split(..., "\n")[0]/fs/linkEncodePath+ 无条件sign?d无对应语义,未追加)有意保留的差异(不改,理由如下):
sign_all默认值true(internal/bootstrap/data/setting.go)false(internal/model/db.ts)true会让所有新装部署立即全站要求签名,而s3.ts/webdav.ts/seed.ts三处 raw_url 出口目前只编码、未补签,必须同批处理。建议单独 PR 决策。raw_url前缀GetApiUrl(ctx)=site_url,返回绝对地址/api/p/...seed_site_url语义是 transfer seed 源),且前端被强制同源部署(scripts/fetch-frontend.mjs固定VITE_API_URL=/),相对地址等价且不需要额外配置。model.GetUrl/fs.Link)/api/pisAuthBoundDownload/raw_url_headers即为它存在),且要处理 CORS 与平台 302/载荷限制。改成 Go 的行为会把这些问题重新引入。base64url(hmac) + ":" + expire,密钥conf.Tokenexpire.hex(hmac),密钥JWT_SECRETJWT_SECRET已走 KV/D1 持久化。未一并修复的同类出口(建议后续)
getItem()的rawUrl还被 S3 网关与 WebDAV 用作 302 目标、被seed.ts用于服务端自取文件。本 PR 已统一它们的路径编码,但没有补签(S3/WebDAV 的客户端会跟随 302 到/api/p,sign_all开启时仍会 401)。根治办法是把补签下沉到getItem()(需要把env传进StorageRequestContext),会改动共享路径,故留给后续 PR。Commits / 提交
acc8446—fix(sign): /fs/get raw_url 缺少下载签名导致下载 401 (#66)(根因修复 +
raw.tsbuildDownProxyUrl判定对齐 +server/fs_get_rawurl_sign.test.ts)058290c—fix(sign): 对齐 Go 的下载路径编码与 link_expiration 语义(
pkg/path.ts、getItem/WebDAV/seed 编码、link_expiration小时与永久语义、down_proxy_url补签条件与多行解析、/fs/link补签、server/sign_go_parity.test.ts)