统一虎牙 App 登录设备画像
This commit is contained in:
+44
-26
@@ -15,7 +15,7 @@
|
||||
|
||||
⚠ 风险与边界(详见 ``docs/HUYA_APP_OVERVIEW.md``):
|
||||
- hdid 全账号固定共享 -> 服务端可设备维度关联账号;
|
||||
- 滑块 UA / session / traceId 用金样本常量 -> 与随机机型画像不自洽;
|
||||
- 滑块 UA / session / traceId / userAction 均按当前画像和本次请求动态生成;
|
||||
- qr_auth(扫码)/dx_auth(短信) 无自动闭环 -> 碰上直接失败(QR_AUTH_REQUIRED)。
|
||||
"""
|
||||
|
||||
@@ -29,7 +29,7 @@ import re
|
||||
import struct
|
||||
import time
|
||||
import uuid
|
||||
from typing import Mapping
|
||||
from collections.abc import Mapping
|
||||
from urllib.parse import parse_qs, quote, urlparse
|
||||
|
||||
import requests
|
||||
@@ -38,10 +38,10 @@ from loguru import logger
|
||||
from .cert_forge import build_p1, decrypt_cert, forge_cert, parse_p1
|
||||
from .cookie_utils import normalize_huya_cookie
|
||||
from .device_fingerprint import account_state_dir, get_huya_sdid
|
||||
from .device_profile import get_profile
|
||||
from .device_profile import get_profile, mobile_user_agent
|
||||
from .dfp_register import DfpRegistrationError, register_device
|
||||
from .envelope_forge import Envelope
|
||||
from .login import HuyaCredentialError, HuyaLoginError, HuyaLoginResult
|
||||
from .login import HuyaLoginError, HuyaLoginResult
|
||||
from .nonce_forge import K1_DEFAULT, gen_nonce
|
||||
from .wup_encoder import build_password_login_wup
|
||||
|
||||
@@ -91,30 +91,28 @@ class HuyaAppQrAuthRequiredError(HuyaAppLoginError):
|
||||
|
||||
|
||||
def _golden_session_assets() -> tuple[dict, str, str]:
|
||||
"""生成本次登录的会话元数据和行为轨迹。"""
|
||||
from .wup_encoder import make_trace_id, make_user_action
|
||||
|
||||
mj = {
|
||||
"associationId": 8193,
|
||||
"funcName": "hypasswordLogin",
|
||||
"group": 1,
|
||||
"id": 4097,
|
||||
"session": 3251699,
|
||||
"session": random.randint(1_000_000, 9_999_999),
|
||||
"step": 1,
|
||||
"stillLogin": False,
|
||||
"traceId": "0b8f098ff64a5bdc-23473-94783833787595172451",
|
||||
"traceId": make_trace_id(),
|
||||
"type": 3,
|
||||
"uid": 0,
|
||||
"userContext": "",
|
||||
}
|
||||
ua = (
|
||||
'{"curl":"登录页","furl":"我的","latitude":"-1.0","longitude":"-1.0","ssid":"",'
|
||||
'"user_action":[{"id":"24","time":"1787595171950","x":"277","y":"1057"},'
|
||||
'{"id":"11","time":"1787595172444","x":"296","y":"952"}]}'
|
||||
)
|
||||
sd = (
|
||||
"PQwemAN9NHkZKoMqVTFUZBIypqMTaQEOrmXr37xQVhQZqrL/gUKEQ11xvE0ju48V8O/"
|
||||
"t9UBGSp27m4+6bP4IiAEnpaR5Rj1kHEfN2SPLPqYZW9vroxUSoAvjJn6ezTP9jWGxxlRDCbt"
|
||||
"Py4Rd6MencYT/pNImVIWK+YbNKZt1O05bHUFhqHf3"
|
||||
)
|
||||
return mj, ua, sd
|
||||
return mj, make_user_action(), ""
|
||||
|
||||
|
||||
def _android_version(device_info: Mapping[str, object]) -> str:
|
||||
parts = str(device_info.get("screen") or "").split(",")
|
||||
return parts[2] if len(parts) >= 3 and parts[2] else "11"
|
||||
|
||||
|
||||
def wup_password_login_raw(
|
||||
@@ -133,14 +131,15 @@ def wup_password_login_raw(
|
||||
注册链失败抛 ``HuyaAppLoginError``,绝不静默回退旧固定值。
|
||||
"""
|
||||
uid_str = account[3:] if account.startswith("hy_") else account
|
||||
dev = dict(device_info) if device_info is not None else get_profile(account)
|
||||
mj, ua, _old_sd = _golden_session_assets()
|
||||
dev = dict(device_info or DEFAULT_GOLDEN_DEV)
|
||||
if not safedeviceid:
|
||||
try:
|
||||
_t1, safedeviceid, registered_device_id = register_device(
|
||||
fingerprint=dev.get("fingerprint"),
|
||||
proxies=proxies,
|
||||
timeout=timeout,
|
||||
device_info=dev,
|
||||
)
|
||||
except DfpRegistrationError as exc:
|
||||
raise HuyaAppLoginError(f"新设备注册失败: {exc}") from exc
|
||||
@@ -160,7 +159,7 @@ def wup_password_login_raw(
|
||||
data=pkt,
|
||||
headers={
|
||||
"Content-Type": "application/multipart-formdata; charset=UTF-8",
|
||||
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 11)",
|
||||
"User-Agent": f"Dalvik/2.1.0 (Linux; U; Android {_android_version(dev)})",
|
||||
"Accept-Encoding": "gzip",
|
||||
},
|
||||
proxies=proxies,
|
||||
@@ -203,7 +202,12 @@ def parse_risk_url(resp: bytes) -> str | None:
|
||||
return (pt or urls)[0]
|
||||
|
||||
|
||||
def solve_safe_auth(risk_url: str, proxies=None, max_retry: int = 3) -> dict:
|
||||
def solve_safe_auth(
|
||||
risk_url: str,
|
||||
proxies=None,
|
||||
max_retry: int = 3,
|
||||
device_info: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
"""解 safe_auth 风控滑块。"""
|
||||
from .verification.solver import (
|
||||
HuyaQrAuthRequiredError,
|
||||
@@ -218,7 +222,7 @@ def solve_safe_auth(risk_url: str, proxies=None, max_retry: int = 3) -> dict:
|
||||
last_err: Exception | None = None
|
||||
for attempt in range(max_retry):
|
||||
solver = HuyaVerificationSolver(
|
||||
ua=APP_UA_MOBILE,
|
||||
ua=mobile_user_agent(device_info or DEFAULT_GOLDEN_DEV),
|
||||
proxies=proxies,
|
||||
app_id=app_id,
|
||||
page_url=risk_url,
|
||||
@@ -268,11 +272,12 @@ def login_cred_with_flow(
|
||||
|
||||
注册只执行一次;safe_auth 通过后的重发继续使用同一组设备字段。
|
||||
"""
|
||||
dev = dict(device_info or DEFAULT_GOLDEN_DEV)
|
||||
dev = dict(device_info) if device_info is not None else get_profile(account)
|
||||
try:
|
||||
_t1, safedeviceid, registered_device_id = register_device(
|
||||
fingerprint=dev.get("fingerprint"),
|
||||
proxies=proxies,
|
||||
device_info=dev,
|
||||
)
|
||||
except DfpRegistrationError as exc:
|
||||
raise HuyaAppLoginError(f"新设备注册失败: {exc}") from exc
|
||||
@@ -301,7 +306,7 @@ def login_cred_with_flow(
|
||||
raise HuyaAppQrAuthRequiredError(
|
||||
f"该账号 App 渠道要求扫码验证(qr_auth),请先在手机虎牙 App 上正常登录一次建立设备信任。"
|
||||
)
|
||||
solve_safe_auth(risk_url, proxies=proxies)
|
||||
solve_safe_auth(risk_url, proxies=proxies, device_info=dev)
|
||||
logger.info("[huya-app] safe_auth 滑块过验成功,重发 WUP 登录...")
|
||||
continue
|
||||
raise HuyaAppLoginError("登录未返回凭据也无风控URL(密码错误或账号状态异常)")
|
||||
@@ -311,7 +316,13 @@ def login_cred_with_flow(
|
||||
class QrRole:
|
||||
"""udblgn.huya.com/qrLgn/* 的 JSON 协议封装。"""
|
||||
|
||||
def __init__(self, pc: bool, sdid: str, proxies: dict | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
pc: bool,
|
||||
sdid: str,
|
||||
proxies: dict | None = None,
|
||||
device_info: Mapping[str, object] | None = None,
|
||||
):
|
||||
self.pc = pc
|
||||
self.sdid = sdid
|
||||
self.s = requests.Session()
|
||||
@@ -327,7 +338,9 @@ class QrRole:
|
||||
self.req_counter = random.randint(40_000_000, 41_000_000)
|
||||
self.s.headers.update(
|
||||
{
|
||||
"User-Agent": UA_PC if pc else APP_UA_MOBILE,
|
||||
"User-Agent": UA_PC
|
||||
if pc
|
||||
else mobile_user_agent(device_info or DEFAULT_GOLDEN_DEV),
|
||||
"Origin": UDB_BASE,
|
||||
"content-type": "application/json;charset=UTF-8",
|
||||
"Accept": "*/*",
|
||||
@@ -483,7 +496,12 @@ class HuyaAppPasswordLogin:
|
||||
)
|
||||
sdid = sdid_obj.sdid if sdid_obj else ""
|
||||
pc = QrRole(pc=True, sdid=sdid, proxies=self.proxies)
|
||||
ph = QrRole(pc=False, sdid=sdid, proxies=self.proxies)
|
||||
ph = QrRole(
|
||||
pc=False,
|
||||
sdid=sdid,
|
||||
proxies=self.proxies,
|
||||
device_info=self.device_info,
|
||||
)
|
||||
beh, page = web_behavior()
|
||||
|
||||
# 4.1 获取 qrId
|
||||
|
||||
Reference in New Issue
Block a user