189 lines
9.7 KiB
Plaintext
189 lines
9.7 KiB
Plaintext
diff --git a/core/huya/elite_session.py b/core/huya/elite_session.py
|
||
index 31f2449..0eb740c 100644
|
||
--- a/core/huya/elite_session.py
|
||
+++ b/core/huya/elite_session.py
|
||
@@ -94,6 +94,13 @@ class HuyaEliteWssSession:
|
||
)
|
||
if not initialized:
|
||
raise RuntimeError(f"虎牙 {kind} WSS 初始化失败")
|
||
+ if kind == "activity" and self.client._launch_guid:
|
||
+ # wsLaunch 的 sGuid 是当前连接的真实会话值,供后续业务 Cookie
|
||
+ # 与 HTTP baseinfo 继续复用,避免沿用登录时的旧 guid。
|
||
+ self.cookie = self.client._cookie_with_guid(
|
||
+ self.cookie, self.client._launch_guid
|
||
+ )
|
||
+ self.guid = self.client._launch_guid
|
||
except Exception:
|
||
self.close()
|
||
raise
|
||
diff --git a/core/huya/wss_client.py b/core/huya/wss_client.py
|
||
index 929c622..c665afd 100644
|
||
--- a/core/huya/wss_client.py
|
||
+++ b/core/huya/wss_client.py
|
||
@@ -26,6 +26,7 @@ from typing import Any, cast
|
||
|
||
import websockets
|
||
|
||
+from .cookie_utils import cookie_pairs, normalize_cookie_pairs
|
||
from .frame_decoder import _decode_taf_struct, _truncate, format_wss_log
|
||
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType
|
||
from .wup_protocol import WupRequest, WupResponse
|
||
@@ -192,6 +193,26 @@ class HuyaWssClient:
|
||
return normalized
|
||
return f"{normalized}; {cookie}"
|
||
|
||
+ @staticmethod
|
||
+ def _cookie_with_guid(cookie: str, guid: str) -> str:
|
||
+ """用当前 WSS 会话的 sGuid 替换 Cookie 中的旧 guid。"""
|
||
+ value = str(guid or "").strip()
|
||
+ if not value:
|
||
+ return cookie or ""
|
||
+ pairs = cookie_pairs(cookie)
|
||
+ replaced = False
|
||
+ normalized: list[tuple[str, str]] = []
|
||
+ for key, item_value in pairs:
|
||
+ if key == "guid":
|
||
+ if not replaced:
|
||
+ normalized.append((key, value))
|
||
+ replaced = True
|
||
+ continue
|
||
+ normalized.append((key, item_value))
|
||
+ if not replaced:
|
||
+ normalized.append(("guid", value))
|
||
+ return normalize_cookie_pairs(normalized)
|
||
+
|
||
@staticmethod
|
||
def _build_biz_user(uid: int, cookie: str):
|
||
"""构造 WSS 业务 UserId(HAR 实证:guid 为空,cookie 不额外加前缀)"""
|
||
@@ -389,7 +410,12 @@ class HuyaWssClient:
|
||
if launch_rsp is None:
|
||
self.logger("[WSS] 活动 wsLaunch 无响应")
|
||
return False
|
||
- await self.send_auth(uid, cookie, sequence=SEQ_ACTIVITY_BIZ)
|
||
+ # wsLaunch 会为当前连接签发新的 sGuid。AUTH、getConfig 和后续业务
|
||
+ # 请求必须使用同一个值;登录 Cookie 里的旧 guid 不能继续复用。
|
||
+ if self._launch_guid:
|
||
+ cookie = self._cookie_with_guid(cookie, self._launch_guid)
|
||
+ guid = self._launch_guid
|
||
+ await self.send_auth(uid, cookie, sequence=SEQ_ACTIVITY_BIZ, guid=guid)
|
||
# 9.1 抓包:getConfig 使用 wsLaunch 响应返回的新 guid;业务 UserId 仍为空 guid。
|
||
config_rsp = await self.call_get_config_activity(
|
||
uid, cookie, guid=self._launch_guid
|
||
@@ -570,7 +596,13 @@ class HuyaWssClient:
|
||
return body[5 : 5 + wup_len]
|
||
return body
|
||
|
||
- async def send_auth(self, uid: int, cookie: str, sequence: int = SEQ_WSLAUNCH):
|
||
+ async def send_auth(
|
||
+ self,
|
||
+ uid: int,
|
||
+ cookie: str,
|
||
+ sequence: int = SEQ_WSLAUNCH,
|
||
+ guid: str = "",
|
||
+ ):
|
||
"""cmd 0x0a AUTH — 对齐 9.1 抓包(828 活动帧)。
|
||
|
||
9.1 活动 AUTH 结构(逐字节核验):
|
||
@@ -591,8 +623,8 @@ class HuyaWssClient:
|
||
os.write_string(1, WSS_COOKIE_UA)
|
||
os.write_string(2, HuyaWssClient._normalize_biz_cookie(cookie))
|
||
guid_match = re.search(r"(?:^|;\s*)guid=([^;]+)", cookie or "")
|
||
- guid = (guid_match.group(1).strip() if guid_match else "") or self._launch_guid
|
||
- os.write_string(3, guid or "")
|
||
+ auth_guid = (guid or "").strip() or (guid_match.group(1).strip() if guid_match else "") or self._launch_guid
|
||
+ os.write_string(3, auth_guid)
|
||
os.write_int8(4, 1)
|
||
os.write_string(5, "HUYA&ZH&2052")
|
||
os.write_string(6, "")
|
||
diff --git "a/docs/HUYA_COOKIE\350\267\257\347\272\277\347\212\266\346\200\201.md" "b/docs/HUYA_COOKIE\350\267\257\347\272\277\347\212\266\346\200\201.md"
|
||
index 69e5423..d67e890 100644
|
||
--- "a/docs/HUYA_COOKIE\350\267\257\347\272\277\347\212\266\346\200\201.md"
|
||
+++ "b/docs/HUYA_COOKIE\350\267\257\347\272\277\347\212\266\346\200\201.md"
|
||
@@ -15,7 +15,7 @@ App 登录可以得到新鲜的 `udb_cred`、`udb_uid`、`yyuid`、`udb_biztoken
|
||
| `udb_cred`、`udb_uid`、`yyuid`、`udb_biztoken` | 已完成 | App 登录与 QR 绑定链 |
|
||
| `sdid`、`hdid` | 已完成 | hydevice / `df/collect` |
|
||
| `udb_deviceid`、`udb_guiddata`、`udb_anobiztoken`、`udb_anouid` | 已完成 | `anon_device.py` |
|
||
-| `guid` | 会话路径完成,回退算法未完全确认 | `wsLaunch` 返回值优先;回退值为结构仿造 |
|
||
+| `guid` | 会话路径已闭合 | 活动 `wsLaunch` 每次签发 32 位 `sGuid`;初始化时统一覆盖 AUTH、getConfig 和后续业务 Cookie,`956a/966a/976a` 等中间段不是本地版本常量 |
|
||
| `__yamid_new` | 已完成 | `room_normal.js` `generate32` Python 复刻 |
|
||
| `game_did` | 已完成 | 35 位、63 字符集规则复刻 |
|
||
| `_yasids` | 格式完成,精确共享种子未确认 | 与 `__yamid_new` 使用同类时钟生成器 |
|
||
@@ -47,10 +47,9 @@ SDK/Python 输出:d07a45de567322edcd42312f38e610be
|
||
## 尚未闭合的工程项
|
||
|
||
1. 将真实 WebView/Chrome 的 `getFeature()` 结果接入登录调用,替代当前的设备画像近似值。
|
||
-2. 明确 `guid` 的 `956a` / `966a` 版本差异和无 `wsLaunch` 时的真实生成规则。
|
||
-3. 让活动会话补齐后的 Cookie 持久化回账号,避免原始 16 字段账号每次任务重新生成设备字段。
|
||
-4. 为 `raw_cookie` 增加重建网页 Cookie 的服务入口。
|
||
-5. 商城下单、支付和支付状态轮询保持单独人工验证,不纳入只读回归。
|
||
+2. 让活动会话补齐后的 Cookie 持久化回账号,避免原始 16 字段账号每次任务重新生成设备字段。
|
||
+3. 为 `raw_cookie` 增加重建网页 Cookie 的服务入口。
|
||
+4. 商城下单、支付和支付状态轮询保持单独人工验证,不纳入只读回归。
|
||
|
||
## 证据入口
|
||
|
||
diff --git a/docs/HUYA_WEB_FIELD_TRACE.md b/docs/HUYA_WEB_FIELD_TRACE.md
|
||
index 8a7f9fe..4c82581 100644
|
||
--- a/docs/HUYA_WEB_FIELD_TRACE.md
|
||
+++ b/docs/HUYA_WEB_FIELD_TRACE.md
|
||
@@ -77,7 +77,7 @@ qimeisdk-web 0.6.3 源码已定位:
|
||
|
||
| 字段 | 来源 | 风控关联性 | 建议 |
|
||
| --- | --- | --- | --- |
|
||
-| `guid` | 浏览器脚本(有固定段 `0a..966a`)| 高(设备与会话绑定) | **必须按结构仿造或复用 wsLaunch 会话值**;随机纯 hex 与真实结构不符 |
|
||
+| `guid` | 活动 `wsLaunch` 响应 `sGuid`;页面 SDK 负责持久化 | 高(设备与会话绑定) | **优先复用当前 wsLaunch 会话值**;`956a/966a/976a` 等中间段随服务端返回变化,不能固化为版本常量 |
|
||
| `__yamid_new` | 浏览器脚本(CBCC 前缀+种子)| 高(与 rootsid 同源)| `room_normal.js` `generate32` 已复刻;精确 rootsid 共享计数仍待确认 |
|
||
| `_qimei_uuid42` | QIMEI SDK 0.6.3 VM 生成器 `e=6064` | 中 | Python 已按时间编码、CRC32 和随机半字节逐项复刻 |
|
||
| `_qimei_fingerprint` | QIMEI SDK 0.6.3 x64hash128(feature_string, 31) | 中 | Python 已复刻,输入字段见路线状态文档 |
|
||
@@ -87,7 +87,7 @@ qimeisdk-web 0.6.3 源码已定位:
|
||
|
||
**当时的下一步记录(部分已完成,保留作历史)**:
|
||
1. `_qimei_fingerprint` 已改为浏览器能力特征串的 `x64hash128`,见路线状态文档。
|
||
-2. `guid` 已优先使用 `wsLaunch` 会话值,回退固定段仍待确认。
|
||
+2. `guid` 已统一使用 `wsLaunch` 会话值;活动 AUTH、getConfig 与后续业务 Cookie 均复用同一返回值。
|
||
3. `__yamid_new` 已定位 `room_normal.js` `generate32` 并完成 Python 复刻。
|
||
## 六、核心字段生成算法逆向结果(2026-09-01 晚,浏览器实测 + 源码定位)
|
||
|
||
diff --git a/tests/test_huya_elite_protocol.py b/tests/test_huya_elite_protocol.py
|
||
index 71a10e6..ce4360d 100644
|
||
--- a/tests/test_huya_elite_protocol.py
|
||
+++ b/tests/test_huya_elite_protocol.py
|
||
@@ -146,3 +146,37 @@ def test_activity_auth_frame_matches_latest_capture_layout():
|
||
|
||
# body 必须与 9.1 828 AUTH 同样以 TAIL_BYTES 收尾
|
||
assert message.body[-len(TAIL_BYTES):] == TAIL_BYTES
|
||
+
|
||
+
|
||
+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[: -len(TAIL_BYTES)])
|
||
+ assert stream.read_head() == (3, TafType.ZERO)
|
||
+ 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
|