Files
live-hub-py/core/huya/wup_encoder.py
T
yml2213 da2cf5a61d feat(huya): 整合 App 协议登录获取 Cookie 全链路并与 Web 登录解耦
- core/huya: 新增 app_login, wup_encoder, nonce_forge, cert_forge, envelope_forge, device_profile, udb_aes
- core/huya/__init__.py: 导出 login_huya_app_password 与 HuyaAppPasswordLogin
- web/backend: 新增 /accounts/app-password-login 与 /accounts/app-password-login/selected 路由及 Schema,与原 Web 密码登录独立分开
- web/frontend: 增加 App 登录 API 与前端界面“App 登录选中”操作,弹窗结果明确区分
- tests: 新增 test_huya_app_login.py 单元测试覆盖全链路
2026-08-26 15:51:30 +08:00

257 lines
6.8 KiB
Python

"""虎牙 App 密码登录 WUP(TAF) 请求帧编码器。
纯 Python 构造符合 TAF/WUP 协议的请求二进制帧。
"""
from __future__ import annotations
import json
import random
import struct
import time as _time
from typing import Any, Dict
# TAF 类型标签
INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03
STRING1, STRING4 = 0x06, 0x07
MAP, LIST = 0x08, 0x09
STRUCT_BEGIN, STRUCT_END = 0x0A, 0x0B
ZERO, SIMPLE_LIST = 0x0C, 0x0D
class _Writer:
"""TAF 输出流。"""
def __init__(self) -> None:
self.buf = bytearray()
def get(self) -> bytes:
return bytes(self.buf)
def head(self, tag: int, dtype: int) -> None:
if tag < 15:
self.buf.append((tag << 4) | dtype)
else:
self.buf.append(0xF0 | dtype)
self.buf.append(tag)
def int8(self, tag: int, v: int) -> None:
if v == 0:
self.head(tag, ZERO)
else:
self.head(tag, INT8)
self.buf += struct.pack("b", v)
def int16(self, tag: int, v: int) -> None:
if -128 <= v <= 127:
self.int8(tag, v)
else:
self.head(tag, INT16)
self.buf += struct.pack(">h", v)
def int32(self, tag: int, v: int) -> None:
if -32768 <= v <= 32767:
self.int16(tag, v)
else:
self.head(tag, INT32)
self.buf += struct.pack(">i", v)
def int64(self, tag: int, v: int) -> None:
if -2147483648 <= v <= 2147483647:
self.int32(tag, v)
else:
self.head(tag, INT64)
self.buf += struct.pack(">q", v)
def string(self, tag: int, s: str) -> None:
b = s.encode("utf-8")
if len(b) > 255:
self.head(tag, STRING4)
self.buf += struct.pack(">I", len(b))
else:
self.head(tag, STRING1)
self.buf += struct.pack("B", len(b))
self.buf += b
def bytes(self, tag: int, b: bytes) -> None:
"""SIMPLE_LIST (byte[])。"""
self.head(tag, SIMPLE_LIST)
self.head(0, INT8)
self.int32(0, len(b))
self.buf += b
def struct_begin(self, tag: int) -> None:
self.head(tag, STRUCT_BEGIN)
def struct_end(self) -> None:
self.head(0, STRUCT_END)
def list_begin(self, tag: int, n: int) -> None:
self.head(tag, LIST)
self.int32(0, n)
def map_begin(self, tag: int, n: int) -> None:
self.head(tag, MAP)
self.int32(0, n)
def _build_meta_json(session: int, trace_id: str) -> str:
"""构造 _wup_data.t0.t2 元数据 JSON。"""
meta: Dict[str, Any] = {
"associationId": 8193,
"funcName": "hypasswordLogin",
"group": 1,
"id": 4097,
"session": session,
"step": 1,
"stillLogin": False,
"traceId": trace_id,
"type": 3,
"uid": 0,
"userContext": "",
}
return json.dumps(meta, ensure_ascii=False, separators=(",", ":"))
def _make_name(uid_str: str) -> str:
"""登录名 = "hy_" + 虎牙号。"""
if uid_str.startswith("hy_"):
return uid_str
return "hy_" + uid_str
def make_user_action(now_ms: int | None = None) -> str:
"""生成一条随机的 userAction 风控行为 JSON。"""
if now_ms is None:
now_ms = int(_time.time() * 1000)
t1 = now_ms
t2 = now_ms + random.randint(200, 900)
return json.dumps(
{
"curl": "登录页",
"furl": "我的",
"latitude": "-1.0",
"longitude": "-1.0",
"ssid": "",
"user_action": [
{"id": "24", "time": str(t1),
"x": str(random.randint(150, 900)),
"y": str(random.randint(800, 1600))},
{"id": "11", "time": str(t2),
"x": str(random.randint(150, 900)),
"y": str(random.randint(800, 1600))},
],
},
ensure_ascii=False,
separators=(",", ":"),
)
def make_trace_id(pid: int = 0) -> str:
"""生成 traceId,格式 `<hex16>-<pid>-<毫秒时间戳>`。"""
return f"{random.getrandbits(64):016x}-{pid}-{int(_time.time() * 1000)}"
def _build_wup_data(w: _Writer, uid_str: str, sha1_password: str,
safedeviceid: str, hdid: str, session: int,
trace_id: str, user_action_json: str,
device_info: Dict[str, str]) -> None:
"""编码 _wup_data struct。"""
meta_json = _build_meta_json(session, trace_id)
name = _make_name(uid_str)
w.struct_begin(0)
# -- t0: 请求头 struct --
w.struct_begin(0)
w.int8(0, 0)
w.string(1, "1.0")
w.string(2, meta_json)
w.string(3, "5008")
w.int8(4, 3)
w.string(5, safedeviceid)
w.string(6, "")
w.string(7, "")
w.string(8, user_action_json)
w.string(9, "")
w.struct_end()
# -- t1: 设备信息 struct --
di = device_info
w.struct_begin(1)
w.string(0, hdid)
w.string(1, di.get("app_version", "13.4.22"))
w.string(2, di.get("sdk_version", "1.0.80138"))
w.string(3, "")
w.string(4, di.get("ip", "127.0.0.1"))
w.string(5, di.get("vendor", "xiaomi"))
w.string(6, "")
w.struct_end()
# -- t2: 屏幕/设备特征 struct --
w.struct_begin(2)
w.int8(0, 1)
w.string(1, di.get("model", "M2102J2SC"))
w.string(2, di.get("fingerprint", ""))
w.string(3, di.get("os", "android"))
w.string(4, di.get("screen", "M2102J2SC,30,11"))
w.string(6, str(di.get("width", "1080")))
w.string(7, str(di.get("height", "2120")))
w.string(8, di.get("device_id", ""))
w.struct_end()
# -- 登录字段 --
w.string(3, name)
w.string(4, sha1_password)
w.list_begin(5, 1)
w.string(0, "5008")
w.int8(6, 1)
w.map_begin(7, 0)
w.bytes(8, b"")
w.struct_end()
def build_password_login_wup(
uid_str: str,
sha1_password: str,
safedeviceid: str,
hdid: str,
session: int,
trace_id: str,
user_action_json: str,
device_info: Dict[str, str],
) -> bytes:
"""构造密码登录的 WUP TAF 请求体。"""
wd = _Writer()
_build_wup_data(wd, uid_str, sha1_password, safedeviceid, hdid,
session, trace_id, user_action_json, device_info)
wup_data = wd.get()
req = _Writer()
req.int32(0, session)
wupdbreq_v0 = req.get()
sb = _Writer()
sb.map_begin(0, 2)
sb.string(0, "_wup_data")
sb.bytes(1, wup_data)
sb.string(0, "wupudbrequest_v0")
sb.bytes(1, wupdbreq_v0)
s_buffer = sb.get()
w = _Writer()
w.int16(1, 3)
w.int8(2, 0)
w.int8(3, 0)
w.int32(4, session)
w.string(5, "huyaudbwebui")
w.string(6, "hypasswordLogin")
w.bytes(7, s_buffer)
w.int32(8, 0)
w.map_begin(9, 0)
w.map_begin(10, 0)
wup_body = w.get()
return struct.pack(">I", 4 + len(wup_body)) + wup_body