Files
live-hub-py/core/huya/web_cookie_fields.py
T

213 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""虎牙网页 Cookie 缺失字段补齐生成器。
账号 App 登录态只有 16 个 udb_* 认证字段,而活动/商城 WSS 需要完整的网页设备态。
浏览器实测(evidence/browser_cookie_harvest/report.md)确认:
- ``udb_deviceid/udb_guiddata/udb_anobiztoken/udb_anouid`` 由匿名设备态协议
core/huya/anon_device.pymiddle + anonymousLogin)签发;
- ``guid/_qimei_uuid42/__yamid_new/game_did`` 由页面 SDK 脚本写 document.cookie
无服务端签发接口。其中 ``guid`` 与会话强绑定:**wsLaunch 响应返回的新 guid 必须
回写进 cookie**,否则 getConfig 无响应(2026-09-01 实测);
``_qimei_uuid42/__yamid_new/game_did`` 为设备标识,随机值格式对齐即可;
- 其余统计/会话字段(SoundValue/Hm_*/rep_cnt 等)服务端仅记录,可随机生成。
2026-09-01 全链路实测:账号 id=58 补齐后 checkUserBindGameAccount 返回
status=200(和平精英已绑定)、getUserScore 返回 status=200(积分 4880)。
"""
from __future__ import annotations
import random
import secrets
import time
# 统计/会话字段生成规则;9.1 抓包值为样例,格式保持对齐
_FILL_MAP: tuple[tuple[str, str], ...] = (
("SoundValue", "0.50"),
("alphaValue", "0.80"),
("isInLiveRoom", "true"),
("udb_passdata", "3"),
("udb_accdata", "undefined"),
)
# 浏览器实测逆向(2026-09-01,见 docs/HUYA_WEB_FIELD_TRACE.md):
# - __yamid_new: room_normal.js generate32UUID-v1 风格,1582-epoch 本地时区时间戳
# + v1 版本位 + 时钟序列 + 48 位随机 node)。浏览器 epoch = new Date(1582,10,15,
# 0,0,0,0).getTime() 实测 -12216643543000GMT+8)。CBCC 前缀是时间戳自然结果。
# - game_did: vplayerUI.js 35 位随机,字符集 64 去掉末尾 '_'63 选 1)。
# - _qimei_uuid42: QIMEI SDK 生成,cookie 值首字节固定 '1a'(本地存储 44hex 截断)。
_SDK_EPOCH_1582 = -12216643543000 # 浏览器实测 JS Date(1582,10,15) 本地时区 ms
_BASE16 = "0123456789ABCDEF"
_GAME_DID_CHARS = (
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
)
def _return_base16(a: int) -> str:
"""JS returnBase(a,16):正整数转大写十六进制;a<=0 返回空串。"""
if a <= 0:
return ""
out = ""
while a > 0:
out = _BASE16[a % 16] + out
a //= 16
return out
def _generate_bits(a: int, b: int, c: int) -> str:
"""JS generateBits(a,b,c):取 a 的 16 进制串 [floor(b/4)..floor(c/4)] 字符,越界补0。"""
d = _return_base16(a)
out = ""
for g in range(b // 4, c // 4 + 1):
out += d[g] if g < len(d) and d[g] != "" else "0"
return out
def _yamid_new_generate32(rng=None) -> str:
"""逐字复刻 room_normal.js generate32__yamid_new 生成器)。"""
import random
r = rng if rng is not None else random.Random()
now_ms = int(time.time() * 1000)
c = now_ms - _SDK_EPOCH_1582
return "".join((
_generate_bits(c, 0, 31),
_generate_bits(c, 32, 47),
_generate_bits(c, 48, 59) + "1",
_generate_bits(r.randrange(4095), 0, 7),
_generate_bits(r.randrange(4095), 0, 7),
_generate_bits(r.randrange(8191), 0, 7)
+ _generate_bits(r.randrange(8191), 8, 15)
+ _generate_bits(r.randrange(8191), 0, 7)
+ _generate_bits(r.randrange(8191), 8, 15)
+ _generate_bits(r.randrange(8191), 0, 15),
))
def _game_did_generate(length: int = 35) -> str:
"""逐字复刻 vplayerUI.js game_did35 位随机,字符集 64 去掉末尾 '_'。"""
import random
out = ""
for _ in range(length):
idx = int(random.random() * (len(_GAME_DID_CHARS) - 1)) # 63 → 排除 '_'
out += _GAME_DID_CHARS[idx]
return out
def _rand_float() -> str:
import random
return f"{random.random():.17f}"
def fill_web_cookie_fields(
cookie: str,
guid: str,
uid: int | str = 0,
*,
keep_guid: bool = True,
) -> str:
"""补齐网页 Cookie 中缺失的字段。
Args:
cookie: 当前 Cookie(可只有 udb_* 认证字段)。
guid: wsLaunch 响应返回的会话 guid(32hex);缺核心字段时必填。
uid: 虎牙 uid,用于 __yaoldyyuid 等字段。
keep_guid: 已存在 guid 且非空时是否保留(默认 True)。
Returns:
补齐后的 Cookie 字符串(原始字段值不变,仅补缺失项)。
"""
pairs: dict[str, str] = {}
for raw in (cookie or "").split(";"):
item = raw.strip()
if not item or "=" not in item:
continue
key, value = item.split("=", 1)
key = key.strip()
if key:
pairs[key] = value.strip()
def add(key: str, value: str):
if value and (key not in pairs or not pairs[key]):
pairs[key] = value
uid = int(uid or 0)
# 4 个核心设备字段
if guid:
if keep_guid and pairs.get("guid"):
pass # 已有真实 guid 则保留
else:
add("guid", str(guid))
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))
# __yamid_new: room_normal.js generate32 精确复刻(UUID-v1 时间戳,含 CBCC 前缀规律)
add("__yamid_new", _yamid_new_generate32())
add("game_did", _game_did_generate()) # 35 位随机(真实长度)
# qimei 皮肤字段
# _qimei_fingerprint: QIMEI SDK SpookyHashV2(canvas指纹,31) 32hex;设备稳定,
# 无真实 canvas 时可随机(格式 32hex 对齐),真实算法见 trace 文档
add("_qimei_fingerprint", secrets.token_hex(16))
add("_qimei_h38", secrets.token_hex(19))
# 会话/统计字段
yasmid = _rand_float()
add("__yasmid", yasmid)
add("__yamid_tt1", yasmid)
add("_rep_cnt", str(random.randint(0, 9)))
add("rep_cnt", str(random.randint(0, 9)))
add("h_unt", str(int(time.time())))
if uid:
add("__yaoldyyuid", str(uid))
# _yasids 与 __yamid_new 同源(共享时间戳生成器,浏览器实测前缀一致)
add("_yasids", "__rootsid%3D" + _yamid_new_generate32())
add("Hm_lvt_51700b6c722f5bb4cf39906a596ea41f", str(int(time.time())))
add("Hm_lpvt_51700b6c722f5bb4cf39906a596ea41f", str(int(time.time())))
add("HMACCOUNT", secrets.token_hex(8).upper())
add("huya_flash_rep_cnt", str(random.randint(1, 400)))
add("huyasp_rep_cnt", str(random.randint(1, 40)))
add("huya_hd_rep_cnt", str(random.randint(1, 40)))
add("huya_web_rep_cnt", str(random.randint(300, 900)))
for key, value in _FILL_MAP:
add(key, value)
return "; ".join(f"{key}={pairs[key]}" for key in pairs)
def missing_web_cookie_fields(cookie: str) -> tuple[str, ...]:
"""返回缺失的网页核心字段(不含统计字段)。"""
from .cookie_utils import cookie_value
core = (
"guid", "_qimei_uuid42", "__yamid_new", "game_did",
"_qimei_fingerprint", "_qimei_h38", "__yasmid", "__yamid_tt1",
"udb_passdata", "_yasids", "HMACCOUNT",
)
return tuple(key for key in core if not cookie_value(cookie, key))
def cookie_fields(cookie: str) -> dict[str, str]:
"""把 Cookie 字符串转 dict(用于测试/调试)。"""
out = {}
for raw in (cookie or "").split(";"):
item = raw.strip()
if not item or "=" not in item:
continue
key, value = item.split("=", 1)
out[key.strip()] = value.strip()
return out
__all__ = [
"cookie_fields",
"fill_web_cookie_fields",
"missing_web_cookie_fields",
]