ksjsb/core/h5_jsbridge.py
2026-07-30 20:25:56 +08:00

275 lines
8.1 KiB
Python
Raw Permalink 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.

"""Nebula H5 `$encode` signInput builder and local JS-VM bridge.
前端真实逻辑在 `main-CZ3ZSK5w.js`
1. 从 cookie 只取白名单设备字段;
2. 加入 `sigCatVer=1` 和接口 query/body
3. 按 `key=value` 字符串字典序拼接;
4. 调 KsGuard/Yoda VM 的 `$encode` 生成 68hex `__NS_sig3`。
这里保留稳定的 signInput 组装规则和本地 VM runner fallback。正常主流程
已经在 `core.h5_sig3` 里用纯 Python 复现 `$encode` 的摘要字段。
"""
from __future__ import annotations
import json
import os
import queue
import subprocess
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Any
H5_COOKIE_KEYS = (
"kpn",
"kpf",
"userId",
"did",
"c",
"appver",
"language",
"mod",
"did_tag",
"egid",
"oDid",
"androidApiLevel",
"newOc",
"browseType",
"socName",
"ftt",
"abi",
"userRecoBit",
"device_abi",
"grant_browse_type",
"iuid",
"rdid",
)
DEFAULT_H5_ENCODE_RUNNER = Path("core/h5_vendor_encode.mjs")
DEFAULT_H5_ENCODE_SERVER = Path("core/h5_vendor_encode_server.mjs")
@dataclass(frozen=True)
class H5EncodeResult:
sign_input: str
sig3: str
c_info: Any = None
class H5JsBridgeEncoder:
"""长驻 Node `$encode` 进程,避免每次签名重复加载 VM chunk。"""
def __init__(
self,
server: str | Path = DEFAULT_H5_ENCODE_SERVER,
node_bin: str | None = None,
timeout: int = 30,
) -> None:
self.server = Path(server)
self.node = node_bin or os.environ.get("NODE_BIN") or "node"
self.timeout = timeout
self._proc: subprocess.Popen[str] | None = None
self._stdout_queue: queue.Queue[str] = queue.Queue()
self._lock = threading.Lock()
self._request_id = 0
def _start(self) -> None:
if self._proc is not None and self._proc.poll() is None:
return
if not self.server.exists():
raise FileNotFoundError(f"H5 encode server not found: {self.server}")
self._stdout_queue = queue.Queue()
self._proc = subprocess.Popen(
[self.node, str(self.server)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
encoding="utf-8",
bufsize=1,
)
def read_stdout() -> None:
assert self._proc is not None
assert self._proc.stdout is not None
for line in self._proc.stdout:
self._stdout_queue.put(line)
thread = threading.Thread(target=read_stdout, name="H5JsBridgeEncoderStdout", daemon=True)
thread.start()
def close(self) -> None:
proc = self._proc
self._proc = None
if proc is None:
return
try:
if proc.stdin:
proc.stdin.close()
except Exception:
pass
try:
if proc.stdout:
proc.stdout.close()
except Exception:
pass
try:
proc.wait(timeout=1)
except Exception:
try:
proc.kill()
except Exception:
pass
def encode(self, sign_input: str) -> H5EncodeResult:
with self._lock:
self._start()
assert self._proc is not None
assert self._proc.stdin is not None
self._request_id += 1
request_id = self._request_id
payload = json.dumps(
{"id": request_id, "signInput": sign_input},
ensure_ascii=False,
separators=(",", ":"),
)
try:
self._proc.stdin.write(payload + "\n")
self._proc.stdin.flush()
except Exception:
self.close()
raise
while True:
try:
line = self._stdout_queue.get(timeout=self.timeout)
except queue.Empty as exc:
self.close()
raise TimeoutError("H5 encode server timed out") from exc
try:
data = json.loads(line)
except json.JSONDecodeError:
continue
if data.get("id") not in {request_id, None}:
continue
if not data.get("ok", True):
raise RuntimeError(f"H5 encode server failed: {data.get('error')}")
sig3 = str(data.get("result") or "")
if len(sig3) != 68:
raise RuntimeError(f"H5 encode server returned invalid sig3: {sig3!r}")
return H5EncodeResult(sign_input=sign_input, sig3=sig3, c_info=data.get("cInfo"))
def _js_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def _h5_io(params: dict[str, Any]) -> str:
parts: list[str] = []
for key, value in params.items():
if key.startswith("__NS"):
continue
if isinstance(value, (dict, list)):
value = ""
parts.append(f"{key}={value}")
return "".join(sorted(parts))
def build_h5_sign_input(
cookie: dict[str, str],
query: dict[str, Any] | None = None,
body: str | dict[str, Any] | list[Any] | None = None,
method: str = "GET",
request_type: str = "json",
) -> str:
"""按前端 `Mc()/io()` 规则构造 `$encode` 的 secPlain。"""
selected = {key: cookie[key] for key in H5_COOKIE_KEYS if cookie.get(key)}
params: dict[str, Any] = {"sigCatVer": 1, **selected, **(query or {})}
method_lower = method.lower()
request_type_lower = request_type.lower()
if request_type_lower == "json":
if method_lower in {"get", "options", "head"}:
if isinstance(body, dict):
params.update(body)
return _h5_io(params)
if body is None:
body_text = ""
elif isinstance(body, str):
body_text = body
else:
body_text = _js_json(body)
return _h5_io(params) + body_text
if request_type_lower == "form" and isinstance(body, dict):
params.update(body)
return _h5_io(params)
def encode_h5_sig3_with_runner(
sign_input: str,
runner: str | Path = DEFAULT_H5_ENCODE_RUNNER,
node_bin: str | None = None,
timeout: int = 30,
) -> H5EncodeResult:
"""调用本地 JS-VM runner 生成 H5 68hex `__NS_sig3`。"""
runner_path = Path(runner)
if not runner_path.exists():
raise FileNotFoundError(f"H5 encode runner not found: {runner_path}")
node = node_bin or os.environ.get("NODE_BIN") or "node"
proc = subprocess.run(
[node, str(runner_path), "-"],
input=sign_input,
text=True,
capture_output=True,
timeout=timeout,
check=False,
)
if proc.returncode != 0:
detail = (proc.stderr or proc.stdout or "").strip()
raise RuntimeError(f"H5 encode runner failed: {detail}")
data = json.loads(proc.stdout)
sig3 = str(data.get("result") or "")
if len(sig3) != 68:
raise RuntimeError(f"H5 encode runner returned invalid sig3: {sig3!r}")
return H5EncodeResult(sign_input=sign_input, sig3=sig3, c_info=data.get("cInfo"))
def h5_sig3_for_request(
cookie: dict[str, str],
query: dict[str, Any] | None = None,
body: str | dict[str, Any] | list[Any] | None = None,
method: str = "GET",
request_type: str = "json",
runner: str | Path = DEFAULT_H5_ENCODE_RUNNER,
node_bin: str | None = None,
timeout: int = 30,
encoder: H5JsBridgeEncoder | None = None,
) -> H5EncodeResult:
sign_input = build_h5_sign_input(
cookie,
query=query,
body=body,
method=method,
request_type=request_type,
)
if encoder is not None:
return encoder.encode(sign_input)
return encode_h5_sig3_with_runner(sign_input, runner=runner, node_bin=node_bin, timeout=timeout)
__all__ = [
"DEFAULT_H5_ENCODE_RUNNER",
"DEFAULT_H5_ENCODE_SERVER",
"H5_COOKIE_KEYS",
"H5EncodeResult",
"H5JsBridgeEncoder",
"build_h5_sign_input",
"encode_h5_sig3_with_runner",
"h5_sig3_for_request",
]