虎牙网页Cookie来源驱动合并与WSS传输门禁

This commit is contained in:
yml2213
2026-09-01 19:17:32 +08:00
parent 8e3c0e69bc
commit 48c37bb8d7
16 changed files with 840 additions and 57 deletions
+173 -5
View File
@@ -37,7 +37,7 @@ import requests
from loguru import logger
from .cert_forge import build_p1, forge_cert
from .cookie_utils import normalize_huya_cookie
from .cookie_utils import cookie_pairs, normalize_cookie_pairs
from .device_fingerprint import account_state_dir, get_huya_sdid, reset_account_state
from .device_profile import get_profile, mobile_user_agent
from .dfp_register import DfpRegistrationError, register_device
@@ -62,6 +62,18 @@ APP_UA_MOBILE = (
"Chrome/149.0.7827.159 Mobile Safari/537.36 huya adr/13.4.22/xiaomi/30"
)
# 9.1 网页入口请求中稳定出现的设备态字段。它们必须来自真实 WebView/浏览器
# 运行态或本次请求的 Set-Cookie,不能由 App 凭据推导或随机生成。
WEB_DEVICE_COOKIE_KEYS = (
"guid",
"udb_guiddata",
"udb_deviceid",
"game_did",
"_qimei_uuid42",
"udb_anobiztoken",
"__yamid_new",
)
SessionAssets = tuple[dict, str, str]
RISK_URL_RE = re.compile(rb"https://aq\.huya\.com/p/safe_auth/[^\x00-\x20\"'\\<>]+")
@@ -457,6 +469,110 @@ class HuyaAppPasswordLogin:
logger.debug(f"登录元数据记录失败: {exc}")
return result
@staticmethod
def _missing_web_device_cookie(cookie: str) -> tuple[str, ...]:
"""返回网页设备态缺失字段名,不暴露任何 Cookie 值。"""
present = {key for key, value in cookie_pairs(cookie) if value}
return tuple(key for key in WEB_DEVICE_COOKIE_KEYS if key not in present)
@staticmethod
def _merge_web_cookie(
session: requests.Session,
biztoken: str,
sdid: str,
hdid: str,
device_info: Mapping[str, object],
*,
cred: bytes | str = b"",
uid: int = 0,
username: str = "",
include_transient: bool = True,
) -> str:
"""合并真实登录凭据与已有网页 Cookie。
抓包中的设备/统计 Cookie 由浏览器或设备指纹链产生,不能用随机值
冒充。认证字段来自本次登录或指纹响应并覆盖旧值;其他字段只有在
Session 或 ``device_info`` 明确提供时才保留,缺失就不写入。
"""
jar = session.cookies
if hasattr(jar, "items"):
pairs = [
(str(key), "" if value is None else str(value))
for key, value in jar.items()
]
else:
pairs = [
(str(getattr(cookie, "name", "")), str(getattr(cookie, "value", "")))
for cookie in jar
if getattr(cookie, "name", "")
]
def set_value(key: str, value: object, *, overwrite: bool = False):
value = "" if value is None else str(value).strip()
if not value:
return
if overwrite:
pairs[:] = [
(item_key, item_value)
for item_key, item_value in pairs
if item_key != key
]
if not any(item_key == key for item_key, _item_value in pairs):
pairs.append((key, value))
raw_cred = (
base64.urlsafe_b64encode(cred).decode("ascii").rstrip("=")
if isinstance(cred, bytes)
else str(cred or "")
)
# 本次登录/指纹响应明确返回的字段,必须覆盖旧 Session 值。
set_value("udb_cred", raw_cred, overwrite=True)
if uid:
set_value("udb_uid", int(uid), overwrite=True)
set_value("yyuid", int(uid), overwrite=True)
set_value("udb_biztoken", biztoken, overwrite=True)
set_value("udb_version", "1.0", overwrite=True)
set_value("udb_origin", "0", overwrite=True)
set_value("udb_status", "1", overwrite=True)
set_value("sdid", sdid, overwrite=True)
set_value("hdid", hdid, overwrite=True)
# 手机号不是网页 passport。只有明确的 hy_账号或 Session 现有值可用。
if username.startswith("hy_"):
set_value("udb_passport", username, overwrite=True)
set_value("username", username, overwrite=True)
# 设备/统计字段不推导、不随机生成;仅转发已知真实值。
optional_keys = (
"guid", "udb_guiddata", "__yamid_new",
"game_did", "_qimei_uuid42", "_qimei_fingerprint", "_qimei_h38",
"udb_anouid", "udb_anobiztoken", "__yasmid", "__yamid_tt1",
"SoundValue", "alphaValue", "isInLiveRoom", "udb_deviceid",
"udb_passdata", "_rep_cnt", "Hm_lvt_51700b6c722f5bb4cf39906a596ea41f",
"HMACCOUNT", "udb_appid", "rep_cnt", "udb_accdata", "h_unt",
"__yaoldyyuid", "_yasids", "huya_flash_rep_cnt", "huyasp_rep_cnt",
"huya_hd_rep_cnt", "Hm_lpvt_51700b6c722f5bb4cf39906a596ea41f",
"huya_web_rep_cnt",
)
for key in optional_keys:
explicit = device_info.get(key)
set_value(key, explicit, overwrite=explicit not in (None, ""))
web_guiddata = str(
device_info.get("web_guiddata") or device_info.get("udb_guiddata") or ""
)
if web_guiddata:
set_value("udb_guiddata", web_guiddata, overwrite=True)
web_device_id = str(device_info.get("web_device_id") or "")
if re.fullmatch(r"w_\d{19}", web_device_id):
set_value("udb_deviceid", web_device_id, overwrite=True)
if not include_transient:
pairs = [
(key, value)
for key, value in pairs
if key != "web_qrlogin_confirm_id"
]
return normalize_cookie_pairs(pairs)
def _login_impl(self) -> HuyaLoginResult:
"""登录主体 (原 login)。"""
acct = self.username
@@ -536,6 +652,21 @@ class HuyaAppPasswordLogin:
f"账号设备指纹获取失败(必须为 hydevice): {detail}"
)
sdid = sdid_obj.sdid
# hydevice may write additional browser cookies through its
# document.cookie bridge. Forward only the values actually
# observed from the runner; never synthesize missing fields.
observed_cookie_keys = sorted(sdid_obj.cookies or {})
logger.info(
"[huya-app] hydevice 实际 Cookie 输出字段: {}",
", ".join(observed_cookie_keys) if observed_cookie_keys else "",
)
merge_device_info = dict(self.device_info)
for key, value in (sdid_obj.cookies or {}).items():
if key in WEB_DEVICE_COOKIE_KEYS or key in {
"udb_appid", "__yasmid", "__yamid_tt1", "udb_anouid",
"udb_anobiztoken", "udb_deviceid",
}:
merge_device_info.setdefault(key, value)
logger.info("[huya-app] cred 已获取,开始二维码绑定流程")
pc = QrRole(pc=True, sdid=sdid, proxies=self.proxies)
ph = QrRole(
@@ -621,7 +752,21 @@ class HuyaAppPasswordLogin:
code="BIZTOKEN_TIMEOUT",
)
# 5) POST /web/cookie/verify 兑换 Cookie
# 5) 先把扫码返回的网页 token 和设备态写回 verify Session。
# verify 响应本身没有 Set-Cookie;浏览器是在请求前就已准备好这些字段。
merged_before_verify = self._merge_web_cookie(
pc.s,
biztoken,
sdid,
sdid_obj.hdid,
merge_device_info,
cred=cred,
uid=uid,
username=acct,
)
pc.s.cookies.clear()
for key, value in cookie_pairs(merged_before_verify):
pc.s.cookies.set(key, value)
verify_resp = pc.s.post(
"https://udblgn.huya.com/web/cookie/verify",
json={"appId": 5002},
@@ -634,8 +779,31 @@ class HuyaAppPasswordLogin:
code="VERIFY_FAILED",
)
cookie_str = normalize_huya_cookie(pc.s.cookies)
if "udb_cred" not in cookie_str and "yyuid" not in cookie_str:
cookie_str = self._merge_web_cookie(
pc.s,
biztoken,
sdid,
sdid_obj.hdid,
merge_device_info,
cred=cred,
uid=uid,
username=acct,
include_transient=False,
)
missing_web = self._missing_web_device_cookie(cookie_str)
if missing_web:
missing_text = ", ".join(missing_web)
logger.error(
f"[huya-app] 账号 {acct} 登录凭据有效,但网页设备态不完整,缺少: {missing_text}"
)
return HuyaLoginResult(
success=False,
message=f"COOKIE_INCOMPLETE: 缺少网页设备态({missing_text}),请在真实虎牙网页/WebView完成一次访问后重试",
code="COOKIE_INCOMPLETE",
sdid=sdid,
context=pc.context,
)
if "udb_cred" not in cookie_str or "yyuid" not in cookie_str:
return HuyaLoginResult(
success=False,
message="Cookie 兑换完成但缺失关键凭据 (udb_cred/yyuid)",
@@ -643,7 +811,7 @@ class HuyaAppPasswordLogin:
)
logger.info(
f"[huya-app] 账号 {acct} 登录成功,获取完整 Cookie ({len(cookie_str)}B)"
f"[huya-app] 账号 {acct} 登录成功,获取网页 Cookie ({len(cookie_str)}B)"
)
return HuyaLoginResult(
success=True,