- scripts/huya_clone_ck.py: root 读取小X分身/真身 WebView Cookie,默认 用 AES-128-ECB(UDB 主密钥前 16B) 解 cred0 注入 udb_cred,新增 --anon-device 补匿名设备字段、--no-cred0 退回旧行为 - core/huya/wss_client.py: 帧层改用浏览器 HUYA.WebSocketCommand(JCE) 结构,vData 直接放 Wup 原包,按 WUP iRequestId 匹配响应,初始化顺序 对齐 RegisterGroup→wsLaunch→VerifyCookie→getConfig→UpdateUserExps - tests: 帧级断言改为新结构,假 WS 按 requestId 兑现 - docs: 新增分身 CK 提取、HAR 提取 CK 与积分中心说明 实测:四个分身 cred0 解出 udb_cred(39/43 字段),WSS PlayMallUI.queryAllocation 返回 code=200、18 个商品;tests -k huya 394 passed
404 lines
14 KiB
Python
404 lines
14 KiB
Python
"""积分中心商城链路回归测试(2026-10-13「开通精英宝典 / 购买幸运币」抓包)。
|
||
|
||
抓包文件不入库,这里把关键帧的字节与字段直接固化在测试里;每条断言的来源
|
||
都写在注释里,改动协议时先核对抓包再改测试。
|
||
"""
|
||
|
||
import asyncio
|
||
import base64
|
||
import struct
|
||
|
||
from core.huya.frame_decoder import _decode_wup_body, _extract_wup
|
||
from core.huya.http_client import (
|
||
WSConnectParaInfo,
|
||
_extract_ws_hosts,
|
||
generate_ws_baseinfo,
|
||
)
|
||
from core.huya.shop_structs import shop_spu_number
|
||
from core.huya.taf_protocol import TafInputStream, TafOutputStream, TafType
|
||
from core.huya.wss_client import (
|
||
HuyaWssClient,
|
||
WssCommand,
|
||
WssMessage,
|
||
)
|
||
from core.huya.wup_protocol import WupResponse
|
||
|
||
UID = 1199668595232
|
||
GUID = "0a8995af9218ce6a340261946294a9e7"
|
||
COOKIE = f"udb_uid={UID}; yyuid={UID}; guid={GUID}"
|
||
PID = 1199650619883
|
||
# 抓包字面值(不要引用实现常量,否则断言会与实现一起漂移)
|
||
POINTS_UA_LITERAL = "webh5&0.0.1&websocket&&h5_/points.html"
|
||
# 旧版本在 vData 末尾拼的尾部字节;JCE 帧不需要,保留常量用于确认已移除
|
||
TAIL_LITERAL = bytes.fromhex("2c36004c5c6600")
|
||
BIZ_PARAMS_LITERAL = '{ "jifencenter": 1 }'
|
||
|
||
# 抓包实测:RegisterGroup(live:0) 与 UpdateUserExps(版本 map) 的 body 逐字节一致
|
||
HEARTBEAT_BODY = bytes.fromhex("09000106066c6976653a301600")
|
||
UPDATE_EXPS_BODY = bytes.fromhex("080001060535383636341606313037323633")
|
||
CONFIG_VERSIONS = {"58664": "107263"}
|
||
|
||
|
||
class _FakeWs:
|
||
"""记录发出的帧,并立刻用预设 body 兑现 RPC future(离线跑完整链路)。"""
|
||
|
||
def __init__(self, client: HuyaWssClient, response: bytes = b""):
|
||
self.client = client
|
||
self.response = response
|
||
self.sent: list[bytes] = []
|
||
|
||
async def send(self, data: bytes):
|
||
self.sent.append(data)
|
||
message = WssMessage.decode(data)
|
||
if message.command != WssCommand.RPC_REQUEST:
|
||
return
|
||
try:
|
||
wup = WupResponse()
|
||
wup.decode(_extract_wup(message.body))
|
||
except Exception: # noqa: BLE001 测试替身:无法解析的帧不兑现
|
||
return
|
||
future = self.client._pending.pop(wup.iRequestId, None)
|
||
if future is not None and not future.done():
|
||
future.set_result(self.response)
|
||
|
||
def last_body(self) -> bytes:
|
||
return WssMessage.decode(self.sent[-1]).body
|
||
|
||
def last_request(self) -> dict:
|
||
return _decode_wup_body(self.last_body())
|
||
|
||
|
||
def _points_client(response: bytes = b"") -> HuyaWssClient:
|
||
client = HuyaWssClient(logger=lambda _message: None)
|
||
client._points_mode = True
|
||
client.ws = _FakeWs(client, response)
|
||
return client
|
||
|
||
|
||
def _wup_response(t_rsp: bytes) -> bytes:
|
||
"""构造一个 tRsp 为给定结构的 WUP 响应(含 4 字节长度前缀)。"""
|
||
buffer = TafOutputStream()
|
||
buffer.write_head(0, TafType.MAP)
|
||
buffer.write_int32(0, 1)
|
||
buffer.write_string(0, "tRsp")
|
||
buffer.write_bytes(1, t_rsp)
|
||
|
||
output = TafOutputStream()
|
||
output.write_int16(1, 3)
|
||
output.write_int8(2, 0)
|
||
output.write_int32(3, 0)
|
||
output.write_int32(4, 1)
|
||
output.write_string(5, "mobileui")
|
||
output.write_string(6, "getConfig")
|
||
output.write_bytes(7, buffer.get_bytes())
|
||
output.write_int32(8, 0)
|
||
output.write_map(9, {})
|
||
output.write_map(10, {})
|
||
body = output.get_bytes()
|
||
return struct.pack(">I", 4 + len(body)) + body
|
||
|
||
|
||
def _config_rsp(versions: dict[str, str]) -> bytes:
|
||
"""mobileui.getConfig 响应:tag0 开关 map、tag1 版本 map(抓包实测)。"""
|
||
output = TafOutputStream()
|
||
output.write_struct_begin(0)
|
||
output.write_map(0, {})
|
||
output.write_map(1, versions)
|
||
output.write_struct_end()
|
||
return _wup_response(output.get_bytes())
|
||
|
||
|
||
def test_register_group_frame_matches_capture():
|
||
async def run():
|
||
client = _points_client()
|
||
await client.send_heartbeat()
|
||
message = WssMessage.decode(client.ws.sent[-1])
|
||
assert message.command == WssCommand.HEARTBEAT_SEND
|
||
assert message.sequence == 0
|
||
assert message.body == HEARTBEAT_BODY
|
||
assert not message.body.endswith(TAIL_LITERAL)
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_shop_ws_launch_carries_real_identity_and_points_ua():
|
||
async def run():
|
||
client = _points_client(_wup_response(b""))
|
||
await client.call_ws_launch_shop(UID, GUID)
|
||
|
||
message = WssMessage.decode(client.ws.sent[-1])
|
||
assert message.command == WssCommand.RPC_REQUEST
|
||
# JCE 帧 lRequestId 固定 0,真正的请求号是 WUP iRequestId=-1
|
||
assert message.sequence == 0
|
||
wup = WupResponse()
|
||
wup.decode(_extract_wup(message.body))
|
||
assert wup.iRequestId == -1
|
||
|
||
decoded = _decode_wup_body(message.body)
|
||
assert (decoded["servant"], decoded["func"]) == ("launch", "wsLaunch")
|
||
t_req = decoded["tReq"]
|
||
# 抓包:lUid=真实 uid、sGuid=真实 guid、UA=h5_/points.html(不再是匿名)
|
||
assert t_req["tag0"] == UID
|
||
assert t_req["tag1"] == GUID
|
||
assert t_req["tag2"] == POINTS_UA_LITERAL
|
||
assert t_req["tag3"] == "HUYA&ZH&2052"
|
||
assert t_req["tag4"] == {
|
||
"tag0": "",
|
||
"tag1": "",
|
||
"tag2": "",
|
||
"tag3": "",
|
||
"tag4": "",
|
||
}
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_get_config_uses_real_user_and_confirms_version_map():
|
||
async def run():
|
||
client = _points_client(_config_rsp(CONFIG_VERSIONS))
|
||
body = await client.call_get_config_shop(uid=UID, guid=GUID, cookie=COOKIE)
|
||
assert body is not None
|
||
assert client._config_versions == CONFIG_VERSIONS
|
||
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == ("mobileui", "getConfig")
|
||
t_req = decoded["tReq"]
|
||
# 抓包:getConfig 的 UserId 带真实 uid/guid/页面 UA/完整 Cookie
|
||
assert t_req["tag0"]["tag0"] == UID
|
||
assert t_req["tag0"]["tag1"] == GUID
|
||
assert t_req["tag0"]["tag3"] == POINTS_UA_LITERAL
|
||
assert GUID in t_req["tag0"]["tag4"]
|
||
assert t_req["tag2"] == "huya"
|
||
|
||
# UpdateUserExps 必须原样回传版本 map(抓包逐字节一致)
|
||
await client.send_confirm(client._config_versions)
|
||
assert client.ws.last_body() == UPDATE_EXPS_BODY
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_create_order_v5_matches_points_capture():
|
||
async def run():
|
||
client = _points_client()
|
||
await client.create_order(
|
||
uid=UID,
|
||
guid=GUID,
|
||
cookie=COOKIE,
|
||
pid=PID,
|
||
spu_id="hy-8328015",
|
||
sku_id=7756811,
|
||
item_count=1,
|
||
)
|
||
|
||
message = WssMessage.decode(client.ws.sent[-1])
|
||
assert message.command == WssCommand.RPC_REQUEST
|
||
|
||
decoded = _decode_wup_body(message.body)
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"shopMiddleUI",
|
||
"createOrderV5",
|
||
)
|
||
t_req = decoded["tReq"]
|
||
assert t_req["tag1"]["tag4"] == 7 # shopAppInfo.scene=7(appInfo 用 7)
|
||
assert t_req["tag3"] == PID
|
||
assert t_req["tag4"] == 7756811
|
||
assert t_req["tag5"] == 1
|
||
assert t_req["tag7"] == "hy-8328015"
|
||
assert t_req["tag8"] == "0"
|
||
assert t_req["tag10"] == 4 # src
|
||
assert t_req["tag12"] == 6 # orderType
|
||
assert t_req["tag13"]["tag4"] == BIZ_PARAMS_LITERAL
|
||
assert t_req["tag16"] == "yellowcarlist"
|
||
assert t_req["tag17"] == {"user_click_seq": "[]"}
|
||
assert t_req["tag18"] == 4 # orderScene
|
||
assert t_req["tag21"]["tag2"] == 1
|
||
assert t_req["tag30"] == 5
|
||
assert t_req["tag31"] == 507
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_pay_order_submit_v5_callback_points_page():
|
||
async def run():
|
||
client = _points_client()
|
||
await client.pay_order_submit(
|
||
uid=UID,
|
||
guid=GUID,
|
||
cookie=COOKIE,
|
||
order_id=11339649,
|
||
pay_type=1,
|
||
pid=PID,
|
||
)
|
||
|
||
message = WssMessage.decode(client.ws.sent[-1])
|
||
assert message.command == WssCommand.RPC_REQUEST
|
||
|
||
decoded = _decode_wup_body(message.body)
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"shopMiddleUI",
|
||
"payOrderSubmitV5",
|
||
)
|
||
t_req = decoded["tReq"]
|
||
assert t_req["tag2"] == 11339649
|
||
assert t_req["tag3"] == "Zfb"
|
||
assert t_req["tag4"] == "QrCode"
|
||
# 抓包:回调指向 m-shop.huya.com/points.html,不是 yaoguo 域名
|
||
assert t_req["tag5"].startswith(
|
||
"https://m-shop.huya.com/points.html#/consumer/paycallback?orderId=11339649"
|
||
)
|
||
assert t_req["tag6"] == {"user_click_seq": "[]"}
|
||
assert t_req["tag7"] == "null"
|
||
assert t_req["tag8"] == ""
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_new_points_rpcs_match_capture():
|
||
async def run():
|
||
client = _points_client()
|
||
|
||
await client.get_supplier_info(uid=UID, guid=GUID, cookie=COOKIE)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"shopMiddleUI",
|
||
"getSupplierInfoV5",
|
||
)
|
||
assert decoded["tReq"]["tag2"] == 87401
|
||
|
||
await client.accept_hy_protocol(uid=UID, guid=GUID, cookie=COOKIE)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"shopMiddleUI",
|
||
"acceptHyProtocolV5",
|
||
)
|
||
assert decoded["tReq"]["tag2"] == 1
|
||
|
||
await client.calc_order_promotion(
|
||
uid=UID,
|
||
guid=GUID,
|
||
cookie=COOKIE,
|
||
pid=PID,
|
||
spu_id="hy-8328015",
|
||
sku_id=7756811,
|
||
)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"shopMiddleUI",
|
||
"calcOrderPromotion",
|
||
)
|
||
t_req = decoded["tReq"]
|
||
assert t_req["tag2"] == 8328015 # spuId 去前缀后的数字
|
||
assert t_req["tag3"] == 7756811
|
||
assert t_req["tag4"] == 1
|
||
assert t_req["tag5"] == PID
|
||
assert (t_req["tag7"], t_req["tag8"], t_req["tag9"]) == (1, 1, 1)
|
||
assert t_req["tag14"] == 8328015
|
||
assert t_req["tag15"] == 9815
|
||
|
||
await client.play_mall_query_allocation(uid=UID, guid=GUID, cookie=COOKIE)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"PlayMallUI",
|
||
"queryAllocation",
|
||
)
|
||
t_req = decoded["tReq"]
|
||
assert t_req["tag1"]["tag1"] == 1001
|
||
assert t_req["tag1"]["tag5"] == 7
|
||
assert t_req["tag1"]["tag6"] == "yellowcarlist"
|
||
assert t_req["tag2"] == "heping_baodian_credit_center_page"
|
||
|
||
await client.play_mall_get_my_promotion(
|
||
uid=UID,
|
||
guid=GUID,
|
||
cookie=COOKIE,
|
||
spu_id="hy-8328015",
|
||
sku_id=7756811,
|
||
price=3000,
|
||
)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"PlayMallUI",
|
||
"getMyPromotion",
|
||
)
|
||
goods = decoded["tReq"]["tag6"]
|
||
assert goods["tag0"] == 87401
|
||
assert goods["tag1"] == 8328015
|
||
assert goods["tag2"] == 7756811
|
||
assert goods["tag3"] == 3000
|
||
assert goods["tag6"] == 9815
|
||
|
||
await client.get_user_profile(uid=UID, guid=GUID, cookie=COOKIE)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"huyauserui",
|
||
"getUserProfile",
|
||
)
|
||
assert decoded["tReq"]["tag1"] == UID
|
||
|
||
await client.get_follow_wechat_status(uid=UID, guid=GUID, cookie=COOKIE)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"wupui",
|
||
"getUserFollowWeChatOfficialStatus",
|
||
)
|
||
assert decoded["tReq"]["tag1"] == 1
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_confirm_frame_carries_version_map_without_legacy_tail():
|
||
async def run():
|
||
client = _points_client()
|
||
await client.send_confirm(CONFIG_VERSIONS)
|
||
message = WssMessage.decode(client.ws.sent[-1])
|
||
assert message.command == WssCommand.CONFIRM_SEND
|
||
assert message.sequence == 0
|
||
assert message.body == UPDATE_EXPS_BODY
|
||
assert not message.body.endswith(TAIL_LITERAL)
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_ws_baseinfo_matches_capture_shape():
|
||
raw = base64.b64decode(generate_ws_baseinfo(UID, GUID, POINTS_UA_LITERAL))
|
||
info = WSConnectParaInfo()
|
||
info.read_from(TafInputStream(raw))
|
||
# 抓包:握手 baseinfo 只有 uid/guid/UA/appSrc/exp 有值,cookie 与 traceId 为空
|
||
assert info.lUid == UID
|
||
assert info.sGuid == GUID
|
||
assert info.sUA == POINTS_UA_LITERAL
|
||
assert info.sAppSrc == "HUYA&ZH&2052"
|
||
assert info.sCookie == ""
|
||
assert info.sTraceId == ""
|
||
assert info.sExp == ""
|
||
|
||
with_exp = base64.b64decode(
|
||
generate_ws_baseinfo(UID, GUID, POINTS_UA_LITERAL, exp="58664.107263")
|
||
)
|
||
parsed = WSConnectParaInfo()
|
||
parsed.read_from(TafInputStream(with_exp))
|
||
assert parsed.sExp == "58664.107263"
|
||
|
||
|
||
def test_query_http_dns_hosts_extraction():
|
||
payload = (
|
||
b"\x1a\x09\x00\x04"
|
||
b"\x06\x17d35bf201-ws.va.huya.com"
|
||
b"\x06\x17d35bf373-ws.va.huya.com"
|
||
b"\x06\x17d35bf35c-ws.va.huya.com"
|
||
b"\x06\x17dcca159d-ws.va.huya.com"
|
||
)
|
||
assert _extract_ws_hosts(payload) == [
|
||
"d35bf201-ws.va.huya.com",
|
||
"d35bf373-ws.va.huya.com",
|
||
"d35bf35c-ws.va.huya.com",
|
||
"dcca159d-ws.va.huya.com",
|
||
]
|
||
assert _extract_ws_hosts(b"") == []
|
||
|
||
|
||
def test_shop_spu_number_strips_prefix():
|
||
assert shop_spu_number("hy-8328015") == 8328015
|
||
assert shop_spu_number("8327356") == 8327356
|
||
assert shop_spu_number("") == 0
|