"""a_bogus 签名器。 两种后端,接口相同、可互换: NodeSigner 调本地 node_signer(真实 webmssdk 跑在伪造浏览器环境里)。 当前唯一可用的实现,已实测能被抖音接受。 ABogusSigner 纯 Python 实现。底层 SM3/RC4/自定义 Base64 已在 crypto.py 验证通过,剩字节拼装顺序待从 ab.js 逆出来。 签名器同时决定 User-Agent:a_bogus 把 UA 编进签名,服务端会交叉校验, 所以请求头的 UA 必须与签名时用的完全一致。 """ from __future__ import annotations import json import subprocess from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Protocol, Sequence class SignError(RuntimeError): pass class SignerNotConfigured(SignError): pass class Signer(Protocol): """client 只依赖这两个成员。""" @property def user_agent(self) -> str: """签名时使用的 UA,请求头必须用同一个。""" ... def sign_url(self, url: str, fp: str = "") -> str: """给一条裸 URL,返回带 a_bogus 的完整 URL。""" ... # --------------------------------------------------------------------------- # # Node 后端 # --------------------------------------------------------------------------- # class NodeSigner: """通过 node_signer/sign.js 签名。 node 启动 + 加载 347KB 的 SDK 大约 0.7s,所以支持 batch 一次签多条, 把这份固定开销摊掉。 """ def __init__(self, signer_dir: str | Path, node: str = "node", timeout: float = 120.0) -> None: self.dir = Path(signer_dir) self.node = node self.timeout = timeout self._ua: str | None = None script = self.dir / "sign.js" if not script.exists(): raise SignerNotConfigured(f"找不到签名脚本: {script}") def _run(self, args: Sequence[str], stdin: str | None = None) -> dict: try: proc = subprocess.run( [self.node, "sign.js", *args], cwd=self.dir, input=stdin, capture_output=True, text=True, encoding="utf-8", timeout=self.timeout, ) except FileNotFoundError as exc: raise SignerNotConfigured(f"找不到 node 可执行文件: {self.node}") from exc except subprocess.TimeoutExpired as exc: raise SignError(f"签名超时(>{self.timeout}s)") from exc out = (proc.stdout or "").strip() if not out: raise SignError(f"签名器无输出,rc={proc.returncode},stderr={proc.stderr[:400]}") try: # sign.js 只在最后一行吐 JSON,前面可能有 SDK 自己的噪声 return json.loads(out.splitlines()[-1]) except ValueError as exc: raise SignError(f"签名器输出不是 JSON: {out[:300]!r}") from exc @property def user_agent(self) -> str: if self._ua is None: probe = self._run(["https://www.douyin.com/aweme/v1/web/aweme/detail/?aweme_id=1", ""]) self._ua = probe.get("userAgent") or "" if not self._ua: raise SignError("签名器没有返回 userAgent") return self._ua def sign_url(self, url: str, fp: str = "") -> str: res = self._run([url, fp]) if not res.get("ok"): raise SignError(f"签名失败: {res.get('error')}") self._ua = res.get("userAgent") or self._ua return res["signedUrl"] def sign_many(self, urls: List[str], fp: str = "") -> List[str]: """一次 node 启动签完多条。""" if not urls: return [] res = self._run(["--batch", fp], stdin=json.dumps(urls)) if not res.get("ok"): raise SignError(f"批量签名失败: {res.get('error')}") self._ua = res.get("userAgent") or self._ua signed = [] for item in res["results"]: if not item.get("ok"): raise SignError(f"某条签名失败: {item.get('error')} url={item.get('rawUrl')}") signed.append(item["signedUrl"]) return signed # --------------------------------------------------------------------------- # # 纯 Python 后端(待补) # --------------------------------------------------------------------------- # @dataclass class ABogusConfig: """a_bogus 算法常量。必须与线上 webmssdk.js 当前版本一致。""" alphabets: Dict[str, str] = field(default_factory=dict) ua_key: bytes = b"" arguments: List[int] = field(default_factory=list) browser_fp: str = "" end_string: str = "" user_agent: str = "" def validate(self) -> None: missing = [n for n in ("alphabets", "ua_key", "arguments", "browser_fp", "end_string", "user_agent") if not getattr(self, n)] if missing: raise SignerNotConfigured("a_bogus 常量未配置,缺少: " + ", ".join(missing)) class ABogusSigner: """纯 Python a_bogus。crypto.py 里的 SM3/RC4/Base64 已验证可用。""" def __init__(self, config: ABogusConfig) -> None: config.validate() self.config = config @property def user_agent(self) -> str: return self.config.user_agent def sign_url(self, url: str, fp: str = "") -> str: raise NotImplementedError( "a_bogus 字节拼装顺序待从 ab.js 逆出。目前请用 NodeSigner。" )