复刻QIMEI UUID42并默认使用无头浏览器

This commit is contained in:
yml2213
2026-09-02 16:59:46 +08:00
parent c62c025ced
commit 17ee943753
9 changed files with 205 additions and 41 deletions
+18 -8
View File
@@ -96,7 +96,12 @@ class Cdp:
return r.get("result", {}).get("value")
def launch(profile: Path, port: int, headless: bool = False) -> tuple[subprocess.Popen, str]:
def launch(profile: Path, port: int, headless: bool = True) -> tuple[subprocess.Popen, str]:
"""启动用于 CDP 抓取的 Chrome。
默认使用无头模式,避免登录/抓取流程弹出窗口;传入 ``headless=False``
仍可在需要人工观察页面时启用可视化窗口。
"""
args = [
str(CHROME),
"--no-first-run", "--no-default-browser-check",
@@ -170,9 +175,9 @@ def cookie_names_from_header(headers: dict) -> list[str]:
return [part.strip().split("=", 1)[0] for part in raw.split(";") if part.strip()]
async def capture_round(profile: Path, tag: str, port: int):
async def capture_round(profile: Path, tag: str, port: int, *, headless: bool = True):
"""抓取一轮:导航序列 + 全量 Cookie 快照。"""
proc, ws_url = launch(profile, port)
proc, ws_url = launch(profile, port, headless=headless)
try:
async with websockets.connect(ws_url, max_size=64 * 1024 * 1024) as raw_ws:
cdp = Cdp(raw_ws)
@@ -294,14 +299,14 @@ async def capture_round(profile: Path, tag: str, port: int):
proc.kill()
async def probe_anon(profile: Path, port: int):
async def probe_anon(profile: Path, port: int, *, headless: bool = True):
"""复用已有 profile:复放 udblgn 匿名流程,解密请求/响应体。
抓取会话内 anonymousLogin 与 web/middle 的 postData 与响应体,
然后带全套 Cookie 用 requests 独立重放 anonymousLogin,检查
服务端是否在匿名流程中下发 guid/udb_guiddata/匿名 token 等字段。
"""
proc, ws_url = launch(profile, port)
proc, ws_url = launch(profile, port, headless=headless)
try:
async with websockets.connect(ws_url, max_size=64 * 1024 * 1024) as raw_ws:
cdp = Cdp(raw_ws)
@@ -562,6 +567,11 @@ def main():
ap.add_argument("--profile", default="/tmp/huya-browser-p1")
ap.add_argument("--port", type=int, default=9333)
ap.add_argument("--out", default=str(OUT_DIR))
ap.add_argument(
"--headed",
action="store_true",
help="显示 Chrome 窗口(默认使用无头模式)",
)
args = ap.parse_args()
OUT_DIR = Path(args.out)
profile = Path(args.profile)
@@ -572,7 +582,7 @@ def main():
if args.mode == "round2" and not (profile / "Default").exists():
print("round2 需要先运行 round1(同一 profile)")
sys.exit(2)
asyncio.run(capture_round(profile, args.mode, args.port))
asyncio.run(capture_round(profile, args.mode, args.port, headless=not args.headed))
elif args.mode == "probe":
# 匿名流程只在"无 udb_deviceid 持久态"的全新 profile 触发;
# probe 模式内部使用临时全新 profile,跑完自动清理。
@@ -581,7 +591,7 @@ def main():
tmp = tempfile.mkdtemp(prefix="huya-probe-")
try:
asyncio.run(probe_anon(Path(tmp), args.port))
asyncio.run(probe_anon(Path(tmp), args.port, headless=not args.headed))
finally:
shutil.rmtree(tmp, ignore_errors=True)
else:
@@ -589,4 +599,4 @@ def main():
if __name__ == "__main__":
main()
main()
+41 -4
View File
@@ -8,7 +8,7 @@
- ``guid/_qimei_uuid42/__yamid_new/game_did`` 由页面 SDK 脚本写 document.cookie,
无服务端签发接口。其中 ``guid`` 与会话强绑定:**wsLaunch 响应返回的新 guid 必须
回写进 cookie**,否则 getConfig 无响应(2026-09-01 实测);
``_qimei_uuid42/__yamid_new/game_did`` 为设备标识,随机值格式对齐即可;
``_qimei_uuid42/__yamid_new/game_did`` 为设备标识,已按浏览器脚本算法生成;
- 其余统计/会话字段(SoundValue/Hm_*/rep_cnt 等)服务端仅记录,可随机生成。
2026-09-01 全链路实测:账号 id=58 补齐后 checkUserBindGameAccount 返回
@@ -20,7 +20,9 @@ from __future__ import annotations
import random
import secrets
import time
import zlib
from collections.abc import Mapping
from datetime import UTC, datetime
# 统计/会话字段生成规则;9.1 抓包值为样例,格式保持对齐
_FILL_MAP: tuple[tuple[str, str], ...] = (
@@ -36,7 +38,7 @@ _FILL_MAP: tuple[tuple[str, str], ...] = (
# + v1 版本位 + 时钟序列 + 48 位随机 node)。浏览器 epoch = new Date(1582,10,15,
# 0,0,0,0).getTime() 实测 -12216643543000(GMT+8)。CBCC 前缀是时间戳自然结果。
# - game_did: vplayerUI.js 35 位随机,字符集 64 去掉末尾 '_'(63 选 1)。
# - _qimei_uuid42: QIMEI SDK 生成,cookie 值首字节固定 '1a'(本地存储 44hex 截断)。
# - _qimei_uuid42: QIMEI SDK 生成,时间字段 + CRC32 替换段 + 随机半字节段。
_SDK_EPOCH_1582 = -12216643543000 # 浏览器实测 JS Date(1582,10,15) 本地时区 ms
_BASE16 = "0123456789ABCDEF"
@@ -263,6 +265,41 @@ def _rand_float() -> str:
return f"{random.random():.17f}"
def _qimei_uuid42_generate(
now_ns: int | None = None,
*,
cookie_enabled: bool = True,
random_value: float | None = None,
) -> str:
"""逐字复刻 QIMEI Web SDK 0.6.3 的 42 位 ``h38`` 生成器。
``now_ns`` 保留旧回退函数的纳秒参数约定;SDK 实际只读取本地时间的
年/月/日/时/分/秒,因此毫秒和纳秒不会进入结果。时间前缀的年份和月份
使用 SDK 的十六进制编码,最后三位来自 ``navigator.cookieEnabled``。
``random_value`` 仅用于固定测试;生产调用沿用 ``Math.random()`` 等价的
Python ``random.random()``,并将理论上的 1.0 夹到合法半字节上界 15。
"""
now_ms = time.time_ns() // 1_000_000 if now_ns is None else now_ns // 1_000_000
current = datetime.fromtimestamp(now_ms / 1000, tz=UTC).astimezone()
# SDK: Number(year % 100).toString(16).padStart(2, "0") +
# Number(month + 1).toString(16) + each remaining component padded to 2.
year_hex = f"{current.year % 100:x}".rjust(2, "0")
month_hex = f"{current.month:x}"
date_parts = "".join(
f"{value:x}".rjust(2, "0")
for value in (current.day, current.hour, current.minute, current.second)
)
prefix = year_hex + month_hex + date_parts + ("100" if cookie_enabled else "000")
value = random.random() if random_value is None else float(random_value)
nibble = min(15, max(0, int(value * 16)))
repeated = f"{nibble:x}" * 20
template = prefix + "----" + repeated + "----"
replacement = f"{zlib.crc32(template.encode('ascii')) & 0xFFFFFFFF:08x}"
return prefix + replacement[:4] + repeated + replacement[4:]
def fill_web_cookie_fields(
cookie: str,
guid: str,
@@ -308,8 +345,8 @@ def fill_web_cookie_fields(
elif "guid" not in pairs:
# guid 结构: '0a' + 10hex + '956a'/'966a' + 16hex(浏览器实测固定段,见 trace 文档)
add("guid", "0a" + secrets.token_hex(5) + "966a" + secrets.token_hex(8))
# _qimei_uuid42: QIMEI SDK 产物,cookie 首字节 '1a' + UUID 布局 32hex + 8hex 附加
add("_qimei_uuid42", "1a" + secrets.token_hex(20))
# _qimei_uuid42: QIMEI SDK 0.6.3 的时间/CRC32/随机半字节组合。
add("_qimei_uuid42", _qimei_uuid42_generate())
# __yamid_new: room_normal.js generate32 精确复刻(UUID-v1 时间戳,含 CBCC 前缀规律)
add("__yamid_new", _yamid_new_generate32())
add("game_did", _game_did_generate()) # 35 位随机(真实长度)
+6 -6
View File
@@ -22,7 +22,7 @@
| `udb_anouid` | anonymousLogin 响应 uid | ✅ 已接入 anon_device |
| `udb_guiddata` | middle URL 第三段 hex32(客户端自选) | ✅ 已接入 anon_device |
| `guid` | `HyUDBWebSDK-Exchange-2.4.js` 写 document.cookie | 会话值已接入;回退生成规则待确认 |
| `_qimei_uuid42` | QIMEI SDK 脚本写 document.cookie | 结构已对齐;SDK 语义待复刻 |
| `_qimei_uuid42` | QIMEI SDK 脚本写 document.cookie | 0.6.3 时间/CRC32/随机半字节算法已复刻 |
| `__yamid_new` | 门户业务脚本写 document.cookie | `room_normal.js` `generate32` 已复刻 |
| `game_did` | 门户业务脚本写 document.cookie | 35 位、63 字符集规则已复刻 |
@@ -41,13 +41,13 @@ udb_appid, udb_guiddata` —— 4 个匿名设备字段已有(anon_device 协
| --- | --- | --- | --- |
| `guid` | 32 hex,`0a7d…` 前缀 | udbres.huya.com SDK 脚本 | `wsLaunch` 会话值已接入;回退规则待确认 |
| `game_did` | 35 位字母数字 | liveapi.ip/getIpLocation 后门户脚本 | 35 位、63 字符集已复刻 |
| `_qimei_uuid42` | 42 hex,`1a901…` 前缀 | QIMEI SDK 脚本 | `1a` + 40hex;精确算法待复刻 |
| `_qimei_uuid42` | 42 hex,时间前缀 | QIMEI SDK 脚本 | 14 位时间前缀 + CRC32 替换段 + 随机半字节 |
| `__yamid_new` | 32 hex 大写 | 门户脚本 | `generate32` 已复刻并通过 WSS 验证 |
浏览器 round1 同一字段多次出现均**值稳定**(跨 profile 稳定):`_qimei_fingerprint`
恒为 `2822a43b5a3bd016fc844218c35c8a74`,`_qimei_uuid42` 前缀恒为 `1a901…`,
`guid` 前缀恒为 `0a7dfaa2…` → 存在固定前缀/哈希结构,**随机值可能被风控识别**,
需还原生成算法。
恒为 `2822a43b5a3bd016fc844218c35c8a74`,`_qimei_uuid42` 按秒内时间前缀稳定,
`guid` 前缀恒为 `0a7dfaa2…`。QIMEI 时间/CRC32/随机半字节生成算法已由 SDK VM
固定输入探针闭合,随机字段仍会在每次生成时变化。
### 3.2 统计/会话字段(随机即可,服务端仅记录)
@@ -108,7 +108,7 @@ cd /Users/yml/codes/live-hub-py
## 六、风险与验证要求
当前实现已经通过活动 WSS 查询链路验证。`_qimei_uuid42` 仍只保证结构,`guid` 的回退值和设备字段持久化仍需继续完善;详细状态见 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md)。
当前实现已经通过活动 WSS 查询链路验证。`guid` 的回退值和设备字段持久化仍需继续完善;详细状态见 [HUYA_COOKIE路线状态.md](HUYA_COOKIE路线状态.md)。
## 七、各通道 Cookie 依赖矩阵(2026-09-01 晚实测)
@@ -30,7 +30,7 @@
同一网页会话内这些值保持稳定:
- `_qimei_uuid42`:42 位;`_qimei_h38`:38 位;`_qimei_fingerprint`:32 位。三者是 QIMEI 设备标识;其中 fingerprint 已按 SDK 能力特征串算法复刻,uuid42 仍只保证结构。
- `_qimei_uuid42`:42 位;`_qimei_h38`:38 位;`_qimei_fingerprint`:32 位。三者是 QIMEI 设备标识;其中 fingerprint 与 uuid42 均已按 SDK 0.6.3 算法复刻。
- `udb_anobiztoken`:344 位 URL-safe 字符串,和 `udb_anouid` 共同属于匿名设备状态,不能用重复 UUID 字节拼接。
- `guid` 与 `udb_guiddata` 均为 32 位,但抓包值不同,不能复用同一画像字段。
- `udb_deviceid` 为 `w_` 加 19 位数字;格式可以校验,但仅凭格式无法得到真实值。
@@ -40,7 +40,7 @@
## 旧实现的问题
旧实现曾在空 Session 中生成 `__yamid_new`、`game_did`、三个 QIMEI 字段、`udb_anobiztoken`、`HMACCOUNT`、`_yasids` 和多个固定计数器。它们只能满足长度测试,不能证明值具有协议语义;当前 fingerprint 已替换为 SDK 算法,uuid42 仍保留结构化回退。
旧实现曾在空 Session 中生成 `__yamid_new`、`game_did`、三个 QIMEI 字段、`udb_anobiztoken`、`HMACCOUNT`、`_yasids` 和多个固定计数器。它们只能满足长度测试,不能证明值具有协议语义;当前 fingerprint 与 uuid42 均已替换为 SDK 0.6.3 算法,仍需以同一会话的线上结果确认风控关联性。
旧实现还把 `device_info["guid32"][:32]` 同时写入 `guid` 和 `udb_guiddata`,并把手机号直接写入 `udb_passport`/`username`。这两点会造成设备身份和账号映射偏差。
+19 -6
View File
@@ -19,7 +19,7 @@ App 登录可以得到新鲜的 `udb_cred`、`udb_uid`、`yyuid`、`udb_biztoken
| `__yamid_new` | 已完成 | `room_normal.js` `generate32` Python 复刻 |
| `game_did` | 已完成 | 35 位、63 字符集规则复刻 |
| `_yasids` | 格式完成,精确共享种子未确认 | 与 `__yamid_new` 使用同类时钟生成器 |
| `_qimei_uuid42` | 结构完成,SDK 内部算法待复刻 | `1a` + 40hex |
| `_qimei_uuid42` | 已完成 | QIMEI SDK 0.6.3 的 14 位时间前缀 + CRC32 替换段 + 20 位随机半字节段 |
| `_qimei_fingerprint` | 算法已复刻 | QIMEI `x64hash128(feature_string, 31)` |
| 统计字段 | 已完成 | 线上查询已接受;部分字段仍是统计用途 |
@@ -46,11 +46,10 @@ SDK/Python 输出:d07a45de567322edcd42312f38e610be
## 尚未闭合的工程项
1. 将真实 WebView/Chrome 的 `getFeature()` 结果接入登录调用,替代当前的设备画像近似值。
2. 对 `_qimei_uuid42` 做 SDK VM 级复刻,当前只保证结构。
3. 明确 `guid` 的 `956a` / `966a` 版本差异和无 `wsLaunch` 时的真实生成规则。
4. 让活动会话补齐后的 Cookie 持久化回账号,避免原始 16 字段账号每次任务重新生成设备字段。
5. 为 `raw_cookie` 增加重建网页 Cookie 的服务入口。
6. 商城下单、支付和支付状态轮询保持单独人工验证,不纳入只读回归。
2. 明确 `guid` 的 `956a` / `966a` 版本差异和无 `wsLaunch` 时的真实生成规则。
3. 让活动会话补齐后的 Cookie 持久化回账号,避免原始 16 字段账号每次任务重新生成设备字段。
4. 为 `raw_cookie` 增加重建网页 Cookie 的服务入口。
5. 商城下单、支付和支付状态轮询保持单独人工验证,不纳入只读回归。
## 证据入口
@@ -58,3 +57,17 @@ SDK/Python 输出:d07a45de567322edcd42312f38e610be
- [Cookie 合并审计](HUYA_COOKIE合并审计与来源矩阵.md)
- [浏览器抓包报告](../evidence/browser_cookie_harvest/report.md)
- [虎牙精英宝典联调指南](HUYA精英宝典联调指南.md)
## `_qimei_uuid42` VM 复刻(2026-09-02)
固定时间 `1700000000000`、固定随机值时,SDK 观测值如下:
```text
random=0 17b0f060d1410085cc000000000000000000003207
random=0.25 17b0f060d14100037f4444444444444444444437df
random=0.5 17b0f060d1410053db888888888888888888883ff6
random=0.75 17b0f060d14100966bcccccccccccccccccccc1cf3
random=.999 17b0f060d141003174ffffffffffffffffffffd85d
```
每个值都可以拆成 `14 + 4 + 20 + 4 = 42` 位。时间前缀由本地时间组成:年份末两位转十六进制并补两位、月份转十六进制、日/时/分/秒转十六进制并各补两位,最后追加 `cookieEnabled ? 100 : 000`。中间 20 位是 `floor(Math.random() * 16)` 对应的十六进制半字节重复。两个 `----` 占位组成模板后,SDK 对模板 ASCII 字节计算标准 CRC32(多项式 `0xEDB88320`),将 8 位小写结果的前后四位分别替换占位,最后调用 `toLocaleLowerCase()`。Python 实现位于 `core/huya/web_cookie_fields.py`,并用上述固定向量回归;随机上界按 15 处理,避免理论输入 `1.0` 产生两位半字节。VM 追踪在 `/tmp/qvmtrace17.out`,生成器入口为 `e=6064`,模板拼接和替换发生在 `m≈5015..5137`。该实现只用于查询链路的设备态生成,不触发兑换、下单或支付。
+20 -13
View File
@@ -33,7 +33,7 @@
| --- | --- | --- |
| `__yamid_new` | 88 条写入中无此 key,但最终 cookie 有值 | **CBCC 前缀 + 递增种子**(见下) |
| `game_did` | 同上 | 30-35 位字母数字 |
| `_qimei_uuid42` | 同上 | `1a` 前缀 + 32hex UUID 结构 + 10hex |
| `_qimei_uuid42` | 同上 | 14 位时间前缀 + 8 位 CRC32 替换值 + 20 位随机半字节 + 4 位 CRC32 尾段 |
## 三、设备字段结构规律(真实样本)
@@ -58,13 +58,13 @@ CBCC 06ECB5 0000 0001 B63016D6162BC990 ← reqable 8.31
`yamid=CBCC6479D9B0...` ↔ `rootsid=CBCC647A8A20...`(同会话,种子差 0xB1≈177)
- `0001` 固定,尾部大段随机
### _qimei_uuid42 = `1a` + 40hex(32hex UUID 结构 + 10hex 附加)
### _qimei_uuid42 = 14 位时间前缀 + CRC32 替换段 + 20 位随机半字节 + CRC32 尾段
```
1a81f140c08100b89fb1be450f640501568684b2ea ← reqable 8.31
1a901123a1f10045427c1c73420287f44f49d11b45 ← round2 fresh
1a901171b051007527940c2acef15dcd2342b6ff ← live 窗口
17b0f060d14100037f4444444444444444444437df ← 固定时间/随机 .25
17b0f060d1410053db888888888888888888883ff6 ← 固定时间/随机 .5
17b0f060d141003174ffffffffffffffffffffd85d ← 固定时间/随机 .999
```
首字节 `1a` 恒定;前 32 hex 符合 RFC 4122 UUID 布局(8-4-4-4-12)。
时间前缀的编码为:年份末两位十六进制(补两位)+ 月份十六进制 + 日/时/分/秒各两位十六进制 + `cookieEnabled` 标志(`100`/`000`)。模板 `prefix + "----" + nibble * 20 + "----"` 的标准 CRC32 小写结果会拆成前后四位填回两个占位;`nibble = floor(Math.random() * 16)`,因此合法范围为 `0` 到 `f`。
### _qimei_fingerprint = x64hash128(feature_string, 31) 32hex
qimeisdk-web 0.6.3 源码已定位:
@@ -79,7 +79,7 @@ qimeisdk-web 0.6.3 源码已定位:
| --- | --- | --- | --- |
| `guid` | 浏览器脚本(有固定段 `0a..966a`)| 高(设备与会话绑定) | **必须按结构仿造或复用 wsLaunch 会话值**;随机纯 hex 与真实结构不符 |
| `__yamid_new` | 浏览器脚本(CBCC 前缀+种子)| 高(与 rootsid 同源)| `room_normal.js` `generate32` 已复刻;精确 rootsid 共享计数仍待确认 |
| `_qimei_uuid42` | QIMEI SDK 0.6.3(`1a` 前缀)| 中 | 前缀 `1a` + UUID 布局 |
| `_qimei_uuid42` | QIMEI SDK 0.6.3 VM 生成器 `e=6064` | 中 | Python 已按时间编码、CRC32 和随机半字节逐项复刻 |
| `_qimei_fingerprint` | QIMEI SDK 0.6.3 x64hash128(feature_string, 31) | 中 | Python 已复刻,输入字段见路线状态文档 |
| `game_did` | 浏览器脚本 | 中 | 长度需对齐(35 变体含 `-`)|
| 统计字段(SoundValue 等)| 页面脚本 | 低 | 随机 OK |
@@ -116,10 +116,17 @@ B = (t=35) => { const e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012
- **长度 35**(旧随机 30 位是错的);字符集 64 去掉末尾 `_`(63 选 1)
- cookie 名 `game_did`,无 cookie 时生成并写入
### ⚠️ _qimei_uuid42 结构已定,算法待深挖
- cookie 值:首字节 `1a` 固定 + 40hex(localStorage 是 44hex,cookie 为截断子串)
- 由 qimeisdk-web 0.6.3 内部 VM 生成(`_qimei_ssid`/`_qimei_last_get_id_time_<16hex>` 同源)
- 当前实现对齐结构(1a 前缀 + 42hex 随机化),精确算法待继续
### ✅ _qimei_uuid42 = QIMEI SDK VM 生成器(已 Python 复刻)
清理 localStorage/cookie 后对 QIMEI SDK 0.6.3 做固定时间、固定随机值追踪,定位到 VM 生成器 `e=6064`。它先拼出:
```text
14 位时间前缀 + "----" + 20 位随机半字节重复串 + "----"
```
随后计算模板 ASCII 字节的标准 CRC32,将 8 位小写结果的前 4 位和后 4 位分别填入占位,最后调用 `toLocaleLowerCase()`。例如 `random=0.25` 的模板是 `17b0f060d14100----44444444444444444444----`,替换值是 `037f37df`,结果为 `17b0f060d14100037f4444444444444444444437df`。`navigator.cookieEnabled` 只影响时间前缀末三位(`100`/`000`)。
- 由 qimeisdk-web 0.6.3 内部 VM 生成(`_qimei_ssid`/`_qimei_last_get_id_time_<16hex>` 同源);字段本身为 42 位小写十六进制。
- Python 实现按 SDK 时间、CRC32 和随机上界复刻,固定向量覆盖 `0`、`.25`、`.5`、`.75`、`.999`。
### ✅ _qimei_fingerprint = x64hash128(feature_string,31),已完成 Python 复刻
- Cookie 输入由屏幕分辨率、浏览器能力、WebGL 和时区字段按 `###` 拼接。
@@ -130,7 +137,7 @@ B = (t=35) => { const e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012
- `__yamid_new` → `_yamid_new_generate32()`(UUID-v1 时间戳复刻)
- `game_did` → `_game_did_generate()`(35 位 63 字符集)
- `_yasids` → 与 `__yamid_new` 同源同刻生成
- `_qimei_uuid42` → `1a` 前缀 + 42hex
- `_qimei_uuid42` → `_qimei_uuid42_generate()`(14 位时间前缀 + CRC32 替换段 + 20 位随机半字节 + CRC32 尾段)
- `guid`(无会话值回退)→ `0a` + 10hex + `966a` + 16hex 结构
## 七、逆向生成器端到端验证(2026-09-01 晚,账号 18397482576 重新登录后)
@@ -138,7 +145,7 @@ B = (t=35) => { const e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012
新版生成器(web_cookie_fields.py commit 8fef04d)产出字段:
- guid=`0a43b01a366c966a7fe26a281e1d3c88`(0a 前缀 + 966a 固定段)
- __yamid_new=`CBCC65D6C5200001F176114BCF181E25`(UUIDv1 时间戳)
- _qimei_uuid42=`1a552b4b...`(1a 前缀)
- _qimei_uuid42=42 位小写十六进制(SDK VM 时间/CRC32 结构)
- game_did=35 位
- _yasids 与 yamid 同源(CBCC65D6C5 前缀一致)
+2 -2
View File
@@ -53,8 +53,8 @@ Cookie 缺少网页设备态(udb_deviceid, udb_guiddata, udb_anobiztoken),已
```
`guid`/`game_did`/`_qimei_uuid42`/`__yamid_new` 四个脚本字段无统一服务端签发接口;
当前登录链已接入 `guid` 会话值、`__yamid_new`、`game_did` 和 fingerprint 算法,
`_qimei_uuid42` 仍使用结构化回退,WSS 判定不要求脚本字段全部来自浏览器。
当前登录链已接入 `guid` 会话值、`__yamid_new`、`game_did`、fingerprint 和
`_qimei_uuid42` 的 SDK 算法,WSS 判定不要求脚本字段全部来自浏览器。
- WSS 握手、`wsLaunch`、`getConfig` 或业务 RPC 任一步失败:任务直接失败并记录具体阶段,便于修复 Cookie 或协议参数。
+53
View File
@@ -0,0 +1,53 @@
from pathlib import Path
from core.huya import browser_harvest
class _FakeStderr:
def readline(self):
return "DevTools listening on ws://127.0.0.1/devtools/browser/test\n"
class _FakeProcess:
stderr = _FakeStderr()
def kill(self):
pass
def test_launch_defaults_to_headless(monkeypatch):
calls = []
def fake_popen(args, **kwargs):
calls.append((args, kwargs))
return _FakeProcess()
def no_http(*args, **kwargs):
raise OSError("test: no CDP endpoint")
monkeypatch.setattr(browser_harvest.subprocess, "Popen", fake_popen)
monkeypatch.setattr(browser_harvest.urllib.request, "urlopen", no_http)
browser_harvest.launch(Path("/tmp/test-huya-profile"), 9555)
args, kwargs = calls[0]
assert "--headless=new" in args
assert kwargs["stdout"] is browser_harvest.subprocess.DEVNULL
def test_launch_can_show_window_when_requested(monkeypatch):
calls = []
def fake_popen(args, **kwargs):
calls.append(args)
return _FakeProcess()
def no_http(*args, **kwargs):
raise OSError("test: no CDP endpoint")
monkeypatch.setattr(browser_harvest.subprocess, "Popen", fake_popen)
monkeypatch.setattr(browser_harvest.urllib.request, "urlopen", no_http)
browser_harvest.launch(Path("/tmp/test-huya-profile"), 9556, headless=False)
assert "--headless=new" not in calls[0]
+44
View File
@@ -2,6 +2,7 @@
from core.huya.web_cookie_fields import (
_qimei_feature_string,
_qimei_uuid42_generate,
_x64hash128,
cookie_fields,
fill_web_cookie_fields,
@@ -46,6 +47,49 @@ def test_fill_replaces_empty_values_and_completes_core_fields():
assert not missing_web_cookie_fields(result)
def test_qimei_uuid42_matches_sdk_vectors_and_random_upper_bound():
base = 1700000000000000000
assert _qimei_uuid42_generate(base, random_value=0.25) == (
"17b0f060d14100037f4444444444444444444437df"
)
assert _qimei_uuid42_generate(base, random_value=0.5) == (
"17b0f060d1410053db888888888888888888883ff6"
)
upper = _qimei_uuid42_generate(base, random_value=1.0)
assert len(upper) == 42
assert upper[18:38] == "f" * 20
assert all(ch in "0123456789abcdef" for ch in upper)
def test_qimei_uuid42_encodes_local_date_and_cookie_flag():
base = 1700000000000000000
assert _qimei_uuid42_generate(base, random_value=0, cookie_enabled=False) == (
"17b0f060d140008b2b00000000000000000000dad6"
)
def test_qimei_uuid42_handles_year_zero_padding():
# 2000-01-01 08:00:00 local time; SDK emits 00 + 1 + 08 + 00 + 00 + 100.
value = _qimei_uuid42_generate(946684800000000000, random_value=0)
assert value[:14] == "00101080000100"
def test_sdk_uuid42_trace_vectors_have_confirmed_outer_layout():
"""锁定 SDK 追踪确认的 14+4+20+4 外层布局,不约束未闭合的替换算法。"""
vectors = {
0.0: "17b0f060d1410085cc000000000000000000003207",
0.25: "17b0f060d14100037f4444444444444444444437df",
0.5: "17b0f060d1410053db888888888888888888883ff6",
0.75: "17b0f060d14100966bcccccccccccccccccccc1cf3",
0.999: "17b0f060d141003174ffffffffffffffffffffd85d",
}
for random_value, value in vectors.items():
assert len(value) == 42
assert value[:14] == "17b0f060d14100"
assert value[18:38] == f"{int(random_value * 16):x}" * 20
assert all(char in "0123456789abcdef" for char in value)
def test_device_profile_builds_stable_webview_feature_approximation():
feature = qimei_feature_from_device_profile({
"vendor": "vivo",