- 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
175 lines
5.7 KiB
Python
175 lines
5.7 KiB
Python
import asyncio
|
||
from unittest.mock import AsyncMock
|
||
|
||
from core.huya.activity_structs import ActPrizeDetailItem, GetActPrizeDetailResp
|
||
from core.huya.http_client import SHOP_BIZ_UA, HuyaHttpClient
|
||
from core.huya.shop_structs import (
|
||
ListPayChannelRsp,
|
||
OrderDetailData,
|
||
OrderDetailOrder,
|
||
OrderDetailRsp,
|
||
ShopCodeRsp,
|
||
)
|
||
from core.huya.taf_protocol import TafInputStream, TafOutputStream, TafType
|
||
from core.huya.wss_client import HuyaWssClient, WssCommand, WssMessage
|
||
from web.backend.services.huya_runner_core import HuyaBatchRunnerCore
|
||
|
||
|
||
def _round_trip(value):
|
||
output = TafOutputStream()
|
||
output.write_struct_begin(0)
|
||
value.write_to(output)
|
||
output.write_struct_end()
|
||
stream = TafInputStream(output.get_bytes())
|
||
_tag, _dtype = stream.read_head()
|
||
value_type = type(value)
|
||
decoded = value_type()
|
||
decoded.read_from(stream)
|
||
_end_tag, end_type = stream.read_head()
|
||
assert end_type == TafType.STRUCT_END
|
||
return decoded
|
||
|
||
|
||
def test_shop_user_matches_latest_wss_identity():
|
||
user = HuyaHttpClient._build_shop_user(123, "yyuid=123; udb_passport=test")
|
||
assert user.sHuYaUA == SHOP_BIZ_UA
|
||
assert user.sGuid == ""
|
||
assert user.lUid == 123
|
||
|
||
|
||
def test_act_prize_detail_round_trip_preserves_exchange_guards():
|
||
prize = ActPrizeDetailItem()
|
||
prize.prizeId = 12861
|
||
prize.name = "幸运币礼包(小)"
|
||
prize.score = 20
|
||
prize.newScore = 20
|
||
prize.leftNum = 15000
|
||
prize.isShowNum = 1
|
||
prize.isCanExchange = 1
|
||
prize.exchangeStartTime = 1782874800
|
||
prize.exchangeEndTime = 1790783400
|
||
response = GetActPrizeDetailResp()
|
||
response.status = 200
|
||
response.msg = "请求成功"
|
||
response.prize = prize
|
||
decoded = _round_trip(response)
|
||
assert decoded.status == 200
|
||
assert decoded.prize is not None
|
||
assert decoded.prize.prizeId == 12861
|
||
assert decoded.prize.newScore == 20
|
||
assert decoded.prize.isCanExchange == 1
|
||
|
||
|
||
def test_order_detail_round_trip_exposes_payment_status():
|
||
order = OrderDetailOrder()
|
||
order.orderId = 10938643
|
||
order.orderStatus = 50
|
||
order.createTime = 1788226648183
|
||
order.payTime = 1788226656000
|
||
order.totalPrice = 3000
|
||
response = OrderDetailRsp()
|
||
response.code = 200
|
||
response.data = OrderDetailData()
|
||
response.data.order = order
|
||
decoded = _round_trip(response)
|
||
assert decoded.code == 200
|
||
assert decoded.order is not None
|
||
assert decoded.order.orderId == 10938643
|
||
assert decoded.order.orderStatus == 50
|
||
assert decoded.order.payTime > 0
|
||
|
||
|
||
def test_shop_preflight_response_contracts():
|
||
channels = ListPayChannelRsp()
|
||
channels.channels = ["Zfb", "Weixin"]
|
||
channels.code = 200
|
||
decoded_channels = _round_trip(channels)
|
||
assert decoded_channels.channels == ["Zfb", "Weixin"]
|
||
assert decoded_channels.code == 200
|
||
|
||
status = ShopCodeRsp()
|
||
status.code = 200
|
||
status.message = ""
|
||
decoded_status = _round_trip(status)
|
||
assert decoded_status.code == 200
|
||
|
||
|
||
def test_wss_requires_web_device_cookie_fields():
|
||
assert not HuyaBatchRunnerCore._has_web_wss_cookie(
|
||
"udb_cred=x; udb_biztoken=x; udb_uid=1; yyuid=1"
|
||
)
|
||
assert HuyaBatchRunnerCore._has_web_wss_cookie(
|
||
"udb_cred=x; udb_biztoken=x; udb_uid=1; yyuid=1; sdid=s; guid=g; "
|
||
"udb_guiddata=gd; udb_deviceid=w_1234567890123456789; game_did=did; "
|
||
"_qimei_uuid42=q; udb_anobiztoken=ab; __yamid_new=y"
|
||
)
|
||
|
||
|
||
def test_activity_auth_frame_matches_latest_capture_layout():
|
||
"""对齐 9.1 活动 AUTH(828 连接)逐字段结构。
|
||
|
||
2026-10-25 对齐 huya-go:tag0 uid -> tag1 UA -> tag2 cookie(STRING4)
|
||
-> tag3 STRING1 guid(32hex) -> tag4 INT32=1 -> tag5 "HUYA&ZH&2052"
|
||
"""
|
||
|
||
async def build_frame():
|
||
client = HuyaWssClient()
|
||
client._activity_mode = True
|
||
client.ws = type("FakeWs", (), {"send": AsyncMock()})()
|
||
await client.send_auth(
|
||
1199664135026,
|
||
"guid=" + "0a" * 16 + "; udb_cred=" + "x" * 300,
|
||
sequence=0x1D000109,
|
||
)
|
||
return client.ws.send.call_args.args[0]
|
||
|
||
message = WssMessage.decode(asyncio.run(build_frame()))
|
||
assert message.command == WssCommand.AUTH
|
||
assert message.sequence == 0
|
||
|
||
stream = TafInputStream(message.body)
|
||
assert stream.read_int64(0) == 1199664135026
|
||
assert stream.read_string(1) == "webh5&0.0.1&websocket&&diypc_52775"
|
||
assert stream.read_head() == (2, TafType.STRING4)
|
||
cookie_len = int.from_bytes(stream.buf.read(4), "big")
|
||
assert cookie_len > 255
|
||
stream.buf.read(cookie_len)
|
||
# tag3 = STRING1 guid(9.1 中为 Cookie 里的 32hex guid)
|
||
guid = stream.read_string(3)
|
||
assert guid == "0a" * 16
|
||
assert stream.read_int32(4) == 1
|
||
assert stream.read_string(5) == "HUYA&ZH&2052"
|
||
|
||
|
||
def test_activity_cookie_guid_is_replaced_by_launch_guid():
|
||
old = "0a" + "1" * 30
|
||
new = "0a" + "2" * 30
|
||
cookie = f"udb_uid=1; guid={old}; x=y; guid={old}"
|
||
|
||
result = HuyaWssClient._cookie_with_guid(cookie, new)
|
||
|
||
assert result == f"udb_uid=1; guid={new}; x=y"
|
||
|
||
|
||
def test_activity_auth_accepts_explicit_launch_guid_over_cookie_guid():
|
||
async def build_frame():
|
||
client = HuyaWssClient()
|
||
client._activity_mode = True
|
||
client.ws = type("FakeWs", (), {"send": AsyncMock()})()
|
||
await client.send_auth(
|
||
1199664135026,
|
||
"guid=" + "0a" * 16 + "; udb_cred=" + "x" * 300,
|
||
sequence=0x1D000109,
|
||
guid="0b" * 16,
|
||
)
|
||
return client.ws.send.call_args.args[0]
|
||
|
||
message = WssMessage.decode(asyncio.run(build_frame()))
|
||
stream = TafInputStream(message.body)
|
||
stream.read_int64(0)
|
||
stream.read_string(1)
|
||
assert stream.read_head() == (2, TafType.STRING4)
|
||
cookie_len = int.from_bytes(stream.buf.read(4), "big")
|
||
stream.buf.read(cookie_len)
|
||
assert stream.read_string(3) == "0b" * 16
|