#!/usr/bin/env python3 """对 NativeAOT 产物做跨平台冒烟(只用标准库,不含 WebSocket,那部分在 smoke.ps1 与集成测试里)。 用法: python3 scripts/smoke.py out/linux-x64/webdrop """ import base64, hashlib, json, os, shutil, struct, subprocess, sys, tempfile, time, urllib.error, urllib.request exe = sys.argv[1] port = int(sys.argv[2]) if len(sys.argv) > 2 else 5199 base = f"http://127.0.0.1:{port}" data_dir = tempfile.mkdtemp(prefix="webdrop-smoke-") def b64(b: bytes) -> str: return base64.urlsafe_b64encode(b).rstrip(b"=").decode() def sha(b: bytes) -> bytes: return hashlib.sha256(b).digest() def check(cond: bool, msg: str): if not cond: raise SystemExit(f"冒烟失败: {msg}") print(f" ok {msg}") def call(method, path, headers=None, body=None): req = urllib.request.Request(base + path, data=body, method=method, headers=headers or {}) try: with urllib.request.urlopen(req, timeout=15) as r: return r.status, r.read() except urllib.error.HTTPError as e: return e.code, e.read() def hdr(role, token, secret): h = {"X-WD-Role": role, "X-WD-Endpoint": b64(secret)} if token: h["Authorization"] = "Bearer " + b64(token) return h def frame(item: bytes, index: int, flags: int, payload: int, fill: int) -> bytes: return bytes([1, flags]) + item + struct.pack(">II", index, payload) + bytes([fill]) * payload env = dict(os.environ, WebDrop__DataDir=data_dir, WebDrop__Captcha__Provider="none") proc = subprocess.Popen([exe, "--urls", base], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: for _ in range(50): try: call("GET", "/api/v1/info") break except Exception: time.sleep(0.2) status, body = call("GET", "/api/v1/info") check(status == 200 and json.loads(body)["protocol"] == 1, "info 可访问") cid = os.urandom(16) read_tok, write_tok, owner_tok = os.urandom(32), os.urandom(32), os.urandom(32) owner_secret, member_secret = os.urandom(32), os.urandom(32) owner_h, member_h = hdr("owner", owner_tok, owner_secret), hdr("write", write_tok, member_secret) create = json.dumps({ "id": b64(cid), "mode": "enc", "read_hash": b64(sha(read_tok)), "write_hash": b64(sha(write_tok)), "owner_hash": b64(sha(owner_tok)), }).encode() status, body = call("POST", "/api/v1/channels", {**owner_h, "Content-Type": "application/json"}, create) check(status == 201, "创建频道") ch = b64(cid) status, body = call("POST", f"/api/v1/channels/{ch}/seats", member_h) check(status == 200 and json.loads(body)["seats_used"] == 2, "成员加入") item = os.urandom(16) f = frame(item, 0, 9, 1040, 0x42) oct_h = {**owner_h, "Content-Type": "application/octet-stream"} status, _ = call("POST", f"/api/v1/channels/{ch}/frames", oct_h, f) check(status == 201, "追加帧 201") status, _ = call("POST", f"/api/v1/channels/{ch}/frames", oct_h, f) check(status == 200, "幂等重发 200") status, body = call("POST", f"/api/v1/channels/{ch}/frames", oct_h, frame(item, 0, 9, 1040, 0x43)) check(status == 409 and json.loads(body)["error"] == "conflict", "内容不同的重复帧 409") status, body = call("GET", f"/api/v1/channels/{ch}/log", member_h) check(status == 200 and body[9:] == f and body[8] == 1, "日志返回完整帧记录") big = os.urandom(16) for i in range(3): flags = 13 if i == 2 else 12 status, _ = call("POST", f"/api/v1/channels/{ch}/frames", {**oct_h, "X-WD-Max-Downloads": "1"}, frame(big, i, flags, 1040, i + 1)) check(status == 201, f"GATED 第 {i} 帧上传") dl = f"/api/v1/channels/{ch}/items/{b64(big)}/frames" status, body = call("GET", dl, member_h) check(status == 200 and len(body) == 3 * (26 + 1040), "GATED 下载得到三帧") status, body = call("GET", dl, member_h) check(status == 410 and json.loads(body)["error"] == "gone", "下载上限生效 410") status, body = call("GET", f"/api/v1/channels/{ch}", hdr("owner", os.urandom(32), owner_secret)) check(status == 401 and json.loads(body)["error"] == "unauthorized", "错误令牌 401") status, _ = call("DELETE", f"/api/v1/channels/{ch}", owner_h) check(status == 204, "删除频道") # 数据目录里不应出现令牌或终端密钥的原值 leaked = False for root, _, files in os.walk(data_dir): for name in files: content = open(os.path.join(root, name), "rb").read() for secret in (owner_tok, write_tok, owner_secret, member_secret): if b64(secret).encode() in content or secret in content: leaked = True check(not leaked, "数据目录里没有令牌或终端密钥的原值") print("\n冒烟通过") finally: proc.terminate() try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() shutil.rmtree(data_dir, ignore_errors=True)