Files
tamabox/dev/theme-builder/check_sigv4.py
T
tamakyi 9c961b3c05 主题素材可放任意 S3 兼容存储,生成器内测通后一键上传
素材位置改成两选一(本地随二进制 / 自行填 Endpoint 的 S3 兼容存储),
R2、MinIO、B2、OSS-COS 的 S3 端点都只差 Endpoint/Region/路径风格。

- serve.py:手写 SigV4(PUT/GET/DELETE/HEAD),新增 /api/s3/test 与
  /api/s3/upload。测试连接依次验连通、写权限、读回一致、匿名可读,
  失败把 S3 的 <Code> 翻成可执行的建议;上传带 immutable 缓存头。
- index.html:新增「素材托管」面板(模式切换 + 7 个字段 + 测试/上传按钮),
  上传集合与导出共用 usedAssetIds();成功后自动回填「素材前缀」。
  密钥只进 sessionStorage 与内存,不进草稿/工程 json/导出 zip。
- 修:usedAssetIds 返回 Set,误用 length/map 导致「还没有素材」。
- 修:字段填完 700ms 内刷新会丢配置 → 失焦与模式切换时立即落盘。
- 自检:check_sigv4.py 与 aws-sdk-go-v2 signer 逐字节对拍(6/6),
  selftest_upload.py 起假 S3 跑通测试连接→上传→读回(16 项),
  mock_s3.py 供手动点面板用。
2026-10-21 21:08:39 +08:00

97 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""校验 serve.py 手写的 SigV4 与 aws-sdk-go-v2 的 signer 逐字节一致。
为什么需要这个:SigV4 错一个字节,服务端只会回一句 SignatureDoesNotMatch,
既不说哪里错、也不会在本地暴露出来 —— 上传功能就成了「点了没反应」。
这里用官方 SDK 当参照实现做对拍,改完签名相关代码跑一遍即可。
用法(项目根):
python dev/theme-builder/check_sigv4.py
⚠️ 用例只放 ASCII 键:aws-sdk-go-v2 裸 SignHTTP 默认会二次转义 URI,
而 S3 客户端内部把 DisableURIPathEscaping 设成了 true(单次编码)。
即同一个路径两种行为,把非 ASCII 键放进来只会比出「SDK 的两个模式之差」。
我们跟随 S3 客户端的单次编码(AWS 文档的 RFC3986 规则),并由生成器把
slug 限制在 [a-z0-9-_],从根上避开这个争议区。
"""
import os
import subprocess
import sys
import urllib.parse
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(HERE))
sys.path.insert(0, HERE)
import serve # noqa: E402
KEY_ID = "AKIAIOSFODNN7EXAMPLE"
SECRET = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
TS = "20130524T000000Z"
EMPTY = serve.EMPTY_SHA256
BODY_HASH = serve._sha256(b"TamaBox theme builder probe\n")
# (说明, method, endpoint, extra cfg, key)
CASES = [
("path-style 桶级 HEAD", "HEAD", "http://127.0.0.1:9000", dict(bucket="tb-assets"), ""),
("path-style 带前缀对象", "PUT", "http://127.0.0.1:9000",
dict(bucket="tb-assets", prefix="themes"), "shiroko/bg1.webp"),
("virtual-host 对象", "GET", "https://s3.us-west-004.backblazeb2.com",
dict(bucket="my-bucket", region="us-west-004", pathStyle=False), "themes/shiroko/bg1.webp"),
("R2 region=auto", "PUT", "https://acc.r2.cloudflarestorage.com",
dict(bucket="cdn", region="auto", prefix="themes"), "x/a.webp"),
("R2 虚拟主机", "GET", "https://acc.r2.cloudflarestorage.com",
dict(bucket="cdn", region="auto", pathStyle=False), "themes/s/t.webp"),
("带端口 https", "DELETE", "https://minio.lan:9443",
dict(bucket="static", region="us-east-1"), "themes/s/t.webp"),
]
def build(endpoint, extra, key):
cfg = serve.S3Config(dict(extra, endpoint=endpoint, accessKey=KEY_ID, secretKey=SECRET))
if key:
path = cfg.object_path(key)
else:
path = ("/" + cfg.bucket) if cfg.path_style else "/"
return cfg, path
def go_sign(payloads):
env = dict(os.environ, AWS_KEY_ID=KEY_ID, AWS_SECRET=SECRET,
GOPROXY="https://goproxy.cn,direct", GOSUMDB="off", GOFLAGS="-mod=mod")
p = subprocess.run(["go", "run", "./dev/sigv4ref"], input="\n".join(payloads),
capture_output=True, text=True, env=env, cwd=ROOT)
if p.returncode != 0:
sys.stderr.write(p.stderr[-2000:])
raise SystemExit("go run 失败(检查 GOPROXY / Go 是否可用)")
return [x for x in p.stdout.strip().splitlines() if x.strip()]
def main():
payloads = []
for _, method, endpoint, extra, key in CASES:
cfg, path = build(endpoint, extra, key)
body = BODY_HASH if method == "PUT" else EMPTY
payloads.append("\t".join([method, cfg.url(path), cfg.region, body, TS]))
go = go_sign(payloads)
if len(go) != len(CASES):
raise SystemExit("返回条数不对:go=%d cases=%d" % (len(go), len(CASES)))
bad = 0
for (desc, method, endpoint, extra, key), g in zip(CASES, go):
cfg, path = build(endpoint, extra, key)
body = BODY_HASH if method == "PUT" else EMPTY
py = serve.sign_v4(cfg, method, path, body, TS, cfg.host)
if g.rsplit("Signature=", 1)[-1] == py.rsplit("Signature=", 1)[-1]:
print("OK %s" % desc)
else:
bad += 1
print("DIFF %s\n go %s\n py %s\n path %s" % (desc, g, py, path))
print("\n%d/%d 一致" % (len(CASES) - bad, len(CASES)))
return 1 if bad else 0
if __name__ == "__main__":
raise SystemExit(main())