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 单元测试覆盖全链路
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
"""虎牙 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
|
||||
Reference in New Issue
Block a user