style: 统一 Ruff 代码格式

This commit is contained in:
yml2213
2026-08-30 21:04:52 +08:00
parent c891ac982e
commit 47e19ed7b2
90 changed files with 5574 additions and 2350 deletions
+6 -1
View File
@@ -6,7 +6,12 @@ from .login_api_wgapi import WgapiLoginAPI
from .login_api_iframe import IframeLoginAPI
from .email_verifier import EmailVerifier
from .activity_client import DouyuActivityClient, DouyuActivityError
from .recharge_api import FishFinRechargeClient, FishFinRechargeConfig, FishFinRechargeConfigError, FishFinRechargeError
from .recharge_api import (
FishFinRechargeClient,
FishFinRechargeConfig,
FishFinRechargeConfigError,
FishFinRechargeError,
)
__all__ = [
"DouyuLogin",
+37 -33
View File
@@ -15,7 +15,9 @@ class CookieEnricher:
CSRF_API = "https://www.douyu.com/japi/carnival/nc/common/generateCsrf"
CSRF_REFERER = "https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0&roomId=9263298"
ACF_CCN_API = "https://www.douyu.com/curl/csrfNlApi/getCsrfCookie"
ACF_CCN_REFERER = "https://www.douyu.com/pages/ord-task-center?clientType=web&panelSource=1&rid=0"
ACF_CCN_REFERER = (
"https://www.douyu.com/pages/ord-task-center?clientType=web&panelSource=1&rid=0"
)
TIMEOUT = (5, 10)
RETRIES = 3
@@ -47,25 +49,27 @@ class CookieEnricher:
logger.warning(f"补CK失败 {attempt}/{max_attempts}: {e}")
if attempt < max_attempts:
self.sleep_interruptible(1)
raise ValueError(f"已重试 {max_attempts} 次仍未补齐CK: {last_error}") from last_error
raise ValueError(
f"已重试 {max_attempts} 次仍未补齐CK: {last_error}"
) from last_error
def generate_csrf_cookie(self) -> str:
"""访问 generateCsrf 接口,从 Set-Cookie 中同步 cvl_csrf_token。"""
logger.info("补齐CSRF Cookie...")
headers = {
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.6,en;q=0.5',
'Origin': 'https://www.douyu.com',
'Referer': self.CSRF_REFERER,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.6,en;q=0.5",
"Origin": "https://www.douyu.com",
"Referer": self.CSRF_REFERER,
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
# 覆盖登录接口的默认表单头,尽量贴近浏览器抓包。
'Content-Type': None,
'X-Requested-With': None,
"Content-Type": None,
"X-Requested-With": None,
}
response = self.request(
'post',
"post",
self.CSRF_API,
headers=headers,
timeout=self.TIMEOUT,
@@ -82,11 +86,11 @@ class CookieEnricher:
preview = body[:200].replace("\n", "\\n")
raise ValueError(f"生成CSRF失败: 响应不是有效 JSON: {preview}") from exc
if payload.get('error') != 0:
if payload.get("error") != 0:
raise ValueError(f"生成CSRF失败: {payload.get('msg', '未知错误')}")
cookies = self.session.cookies.get_dict()
csrf_token = cookies.get('cvl_csrf_token', '')
csrf_token = cookies.get("cvl_csrf_token", "")
if not csrf_token:
raise ValueError("生成CSRF失败: 响应没有 cvl_csrf_token")
@@ -97,25 +101,25 @@ class CookieEnricher:
"""访问 getCsrfCookie 接口,从 Set-Cookie 中同步 acf_ccn。"""
logger.info("补齐 acf_ccn Cookie...")
headers = {
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Priority': 'u=1, i',
'Referer': self.ACF_CCN_REFERER,
'Sec-CH-UA': '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
'Sec-CH-UA-Mobile': '?0',
'Sec-CH-UA-Platform': '"macOS"',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Priority": "u=1, i",
"Referer": self.ACF_CCN_REFERER,
"Sec-CH-UA": '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"macOS"',
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
# 该接口抓包没有 Origin、表单 Content-Type 和 X-Requested-With。
'Origin': None,
'Content-Type': None,
'X-Requested-With': None,
"Origin": None,
"Content-Type": None,
"X-Requested-With": None,
}
response = self.request(
'get',
"get",
self.ACF_CCN_API,
headers=headers,
timeout=self.TIMEOUT,
@@ -123,10 +127,10 @@ class CookieEnricher:
response.raise_for_status()
cookies = self.session.cookies.get_dict()
acf_ccn = cookies.get('acf_ccn', '') or response.cookies.get('acf_ccn', '')
if acf_ccn and not cookies.get('acf_ccn'):
acf_ccn = cookies.get("acf_ccn", "") or response.cookies.get("acf_ccn", "")
if acf_ccn and not cookies.get("acf_ccn"):
# 极少数情况下响应 Cookie 未合入 get_dict,手动补到斗鱼域名下。
self.session.cookies.set('acf_ccn', acf_ccn, domain='.douyu.com', path='/')
self.session.cookies.set("acf_ccn", acf_ccn, domain=".douyu.com", path="/")
if not acf_ccn:
raise ValueError("补齐 acf_ccn 失败: 响应没有 acf_ccn")
+9 -9
View File
@@ -25,21 +25,21 @@ DOUYU_RC4_KEY = "7TkbRSEWvVWebXbr"
def md5(text: str) -> str:
"""MD5加密"""
return hashlib.md5(text.encode('utf-8')).hexdigest()
return hashlib.md5(text.encode("utf-8")).hexdigest()
def rsa_encrypt(text: str, public_key: str = DOUYU_RSA_PUBLIC_KEY) -> str:
"""RSA加密"""
key = RSA.import_key(public_key)
cipher = PKCS1_v1_5.new(key)
encrypted = cipher.encrypt(text.encode('utf-8'))
return base64.b64encode(encrypted).decode('utf-8')
encrypted = cipher.encrypt(text.encode("utf-8"))
return base64.b64encode(encrypted).decode("utf-8")
def aes_encrypt(text: str, key: str = DOUYU_AES_KEY) -> str:
"""AES加密"""
key_bytes = key.encode('utf-8')
text_bytes = text.encode('utf-8')
key_bytes = key.encode("utf-8")
text_bytes = text.encode("utf-8")
# 填充到16的倍数
padding_len = 16 - (len(text_bytes) % 16)
@@ -47,7 +47,7 @@ def aes_encrypt(text: str, key: str = DOUYU_AES_KEY) -> str:
cipher = AES.new(key_bytes, AES.MODE_ECB)
encrypted = cipher.encrypt(text_bytes)
return base64.b64encode(encrypted).decode('utf-8')
return base64.b64encode(encrypted).decode("utf-8")
def encrypt_username(username: str) -> str:
@@ -62,7 +62,7 @@ def encrypt_password(password: str) -> str:
def encrypt_nickname_or_phone(text: str) -> str:
"""加密昵称或手机号"""
key = DOUYU_RC4_KEY.encode('utf-8')
key = DOUYU_RC4_KEY.encode("utf-8")
cipher = ARC4.new(key)
encrypted = cipher.encrypt(text.encode('utf-8'))
return base64.b64encode(encrypted).decode('utf-8')
encrypted = cipher.encrypt(text.encode("utf-8"))
return base64.b64encode(encrypted).decode("utf-8")
+20 -18
View File
@@ -47,7 +47,9 @@ class LoginAPIStrategy(ABC):
...
@abstractmethod
def build_first_login_data(self, username: str, password: str, referer: str) -> dict:
def build_first_login_data(
self, username: str, password: str, referer: str
) -> dict:
"""构建第一次登录请求参数(获取极验参数)"""
...
@@ -85,11 +87,11 @@ class LoginAPIStrategy(ABC):
Returns:
(gt, challenge, code_token)
"""
geetest_data = payload.get('data', {}).get('geetest', {})
code_data = geetest_data.get('code_data', {})
gt = code_data.get('gt', '')
challenge = code_data.get('challenge', '')
code_token = geetest_data.get('code_token', '')
geetest_data = payload.get("data", {}).get("geetest", {})
code_data = geetest_data.get("code_data", {})
gt = code_data.get("gt", "")
challenge = code_data.get("challenge", "")
code_token = geetest_data.get("code_token", "")
return gt, challenge, code_token
def extract_remote_code(self, payload: dict) -> str:
@@ -101,7 +103,7 @@ class LoginAPIStrategy(ABC):
Returns:
remote_code 字符串
"""
return payload.get('data', {}).get('remoteLogin', {}).get('code', '')
return payload.get("data", {}).get("remoteLogin", {}).get("code", "")
def extract_login_url(self, payload: dict) -> str:
"""从验证码响应提取登录 URL
@@ -112,27 +114,27 @@ class LoginAPIStrategy(ABC):
Returns:
登录回调 URL
"""
data = payload.get('data', {})
data = payload.get("data", {})
if not isinstance(data, dict):
return ''
return ""
# `remoteLogin` 与“跳过绑定手机号”完成后都会返回同类回调地址,
# 但不同版本接口的字段命名不完全一致。
for key in ('url', 'loginUrl', 'login_url', 'redirectUrl', 'redirect_url'):
for key in ("url", "loginUrl", "login_url", "redirectUrl", "redirect_url"):
value = data.get(key)
if isinstance(value, str) and value:
return value
return ''
return ""
def extract_mobile_bind_unique_key(self, payload: dict) -> str:
"""提取服务端要求绑定手机号时返回的一次性继续登录标识。"""
data = payload.get('data', {})
data = payload.get("data", {})
if not isinstance(data, dict):
return ''
return ""
def find_unique_key(value: object) -> str:
if isinstance(value, dict):
unique_key = value.get('uniqueKey') or value.get('unique_key')
unique_key = value.get("uniqueKey") or value.get("unique_key")
if isinstance(unique_key, str) and unique_key:
return unique_key
for child in value.values():
@@ -144,14 +146,14 @@ class LoginAPIStrategy(ABC):
result = find_unique_key(child)
if result:
return result
return ''
return ""
return find_unique_key(data)
def build_skip_mobile_bind_data(self, unique_key: str) -> dict:
"""构建跳过手机号绑定、继续完成网页登录的请求参数。"""
return {
'type': '3',
'uniqueKey': unique_key,
'biz_type': '1',
"type": "3",
"uniqueKey": unique_key,
"biz_type": "1",
}
+44 -42
View File
@@ -42,9 +42,11 @@ class IframeLoginAPI(LoginAPIStrategy):
def encrypt_username(self, username: str) -> str:
"""RC4 加密后 URL 编码"""
encrypted = encrypt_nickname_or_phone(username)
return urllib.parse.quote(encrypted, safe='')
return urllib.parse.quote(encrypted, safe="")
def build_first_login_data(self, username: str, password: str, referer: str) -> dict:
def build_first_login_data(
self, username: str, password: str, referer: str
) -> dict:
"""构建第一次登录参数(获取极验参数)
注:第一次登录使用 wgapi 接口,所以用户名用 base64 编码(而非 URL 编码)。
@@ -52,17 +54,17 @@ class IframeLoginAPI(LoginAPIStrategy):
# 第一次登录用 wgapi 接口,需要 base64 编码的用户名
encrypted_username = encrypt_nickname_or_phone(username)
return {
'type': '1',
'nicknameOrPhoneEncrypt': encrypted_username,
'password': encrypt_password(password),
'biz_type': '1',
'room_id': '0',
'redirect_url': referer,
't': str(int(time.time() * 1000)),
'client_id': '1',
'did': '',
'lang': '',
'isMultiAccount': '0',
"type": "1",
"nicknameOrPhoneEncrypt": encrypted_username,
"password": encrypt_password(password),
"biz_type": "1",
"room_id": "0",
"redirect_url": referer,
"t": str(int(time.time() * 1000)),
"client_id": "1",
"did": "",
"lang": "",
"isMultiAccount": "0",
}
def build_second_login_data(
@@ -81,46 +83,46 @@ class IframeLoginAPI(LoginAPIStrategy):
登录使用 wgapi 接口,参数格式与 wgapi 策略一致。
"""
# seccode 格式: validate|jordanrequests 会自动 URL 编码)
seccode_value = validate + '|jordan'
seccode_value = validate + "|jordan"
# 使用 base64 编码(wgapi 接口要求),而不是 URL 编码
encrypted_username = encrypt_nickname_or_phone(username)
return {
'type': '1',
'nicknameOrPhoneEncrypt': encrypted_username,
'password': encrypt_password(password),
'room_id': '0',
'code_type': '1',
'code_token': code_token,
'gt_version': 'v3',
'geetest_challenge': challenge,
'geetest_validate': validate,
'geetest_seccode': seccode_value,
'code_data[geetest_challenge]': challenge,
'code_data[geetest_validate]': validate,
'code_data[geetest_seccode]': seccode_value,
'code_data[gt_version]': 'v3',
'code_data[code]': '',
'redirect_url': referer,
't': str(int(time.time() * 1000)),
'client_id': '1',
'did': '',
'lang': '',
'isMultiAccount': '0',
'biz_type': '1',
"type": "1",
"nicknameOrPhoneEncrypt": encrypted_username,
"password": encrypt_password(password),
"room_id": "0",
"code_type": "1",
"code_token": code_token,
"gt_version": "v3",
"geetest_challenge": challenge,
"geetest_validate": validate,
"geetest_seccode": seccode_value,
"code_data[geetest_challenge]": challenge,
"code_data[geetest_validate]": validate,
"code_data[geetest_seccode]": seccode_value,
"code_data[gt_version]": "v3",
"code_data[code]": "",
"redirect_url": referer,
"t": str(int(time.time() * 1000)),
"client_id": "1",
"did": "",
"lang": "",
"isMultiAccount": "0",
"biz_type": "1",
}
def build_send_email_data(self, code: str) -> dict:
"""构建发送邮箱验证参数"""
return {'cacheId': code}
return {"cacheId": code}
def build_verify_data(self, code: str, captcha: str) -> dict:
"""构建提交验证码参数"""
return {
'veify_type': '2', # e 语言原文如此,斗鱼接口拼写
'phoneCaptcha': captcha,
'client_id': '1',
'isMultiAccount': '0',
'cacheId': code,
"veify_type": "2", # e 语言原文如此,斗鱼接口拼写
"phoneCaptcha": captcha,
"client_id": "1",
"isMultiAccount": "0",
"cacheId": code,
}
+44 -42
View File
@@ -36,20 +36,22 @@ class WgapiLoginAPI(LoginAPIStrategy):
"""RC4 加密后 base64 编码"""
return encrypt_nickname_or_phone(username)
def build_first_login_data(self, username: str, password: str, referer: str) -> dict:
def build_first_login_data(
self, username: str, password: str, referer: str
) -> dict:
"""构建第一次登录参数"""
return {
'type': '1',
'nicknameOrPhoneEncrypt': self.encrypt_username(username),
'password': encrypt_password(password),
'biz_type': '1',
'room_id': '0',
'redirect_url': referer,
't': str(int(time.time() * 1000)),
'client_id': '1',
'did': '',
'lang': '',
'isMultiAccount': '0',
"type": "1",
"nicknameOrPhoneEncrypt": self.encrypt_username(username),
"password": encrypt_password(password),
"biz_type": "1",
"room_id": "0",
"redirect_url": referer,
"t": str(int(time.time() * 1000)),
"client_id": "1",
"did": "",
"lang": "",
"isMultiAccount": "0",
}
def build_second_login_data(
@@ -65,44 +67,44 @@ class WgapiLoginAPI(LoginAPIStrategy):
) -> dict:
"""构建第二次登录参数(带极验验证)"""
return {
'type': '1',
'nicknameOrPhoneEncrypt': self.encrypt_username(username),
'password': encrypt_password(password),
'room_id': '0',
'code_type': '1',
'code_token': code_token,
'gt_version': 'v3',
'geetest_challenge': challenge,
'geetest_validate': validate,
'geetest_seccode': seccode,
'code_data[geetest_challenge]': challenge,
'code_data[geetest_validate]': validate,
'code_data[geetest_seccode]': seccode,
'code_data[gt_version]': 'v3',
'code_data[code]': '',
'redirect_url': referer,
't': str(int(time.time() * 1000)),
'client_id': '1',
'did': '',
'lang': '',
'isMultiAccount': '0',
'biz_type': '1',
"type": "1",
"nicknameOrPhoneEncrypt": self.encrypt_username(username),
"password": encrypt_password(password),
"room_id": "0",
"code_type": "1",
"code_token": code_token,
"gt_version": "v3",
"geetest_challenge": challenge,
"geetest_validate": validate,
"geetest_seccode": seccode,
"code_data[geetest_challenge]": challenge,
"code_data[geetest_validate]": validate,
"code_data[geetest_seccode]": seccode,
"code_data[gt_version]": "v3",
"code_data[code]": "",
"redirect_url": referer,
"t": str(int(time.time() * 1000)),
"client_id": "1",
"did": "",
"lang": "",
"isMultiAccount": "0",
"biz_type": "1",
}
def build_send_email_data(self, code: str) -> dict:
"""构建发送邮箱验证参数"""
return {
'code': code,
'client_id': '1',
"code": code,
"client_id": "1",
}
def build_verify_data(self, code: str, captcha: str) -> dict:
"""构建提交验证码参数"""
return {
'verify_type': '2',
'captcha': captcha,
'isMultiAccount': '0',
'code': code,
'client_id': '1',
'redirect_url': '//www.douyu.com/api/passport/login',
"verify_type": "2",
"captcha": captcha,
"isMultiAccount": "0",
"code": code,
"client_id": "1",
"redirect_url": "//www.douyu.com/api/passport/login",
}
+4 -4
View File
@@ -24,7 +24,7 @@ def parse_proxy_response(text: str) -> tuple[list[str], Optional[str]]:
code = data.get("code")
msg = data.get("msg", "") or ""
if code != 0 and ("白名单" in msg or "添加白名单" in msg):
ip_match = re.search(r'(\d+\.\d+\.\d+\.\d+)', msg)
ip_match = re.search(r"(\d+\.\d+\.\d+\.\d+)", msg)
if ip_match:
return [], ip_match.group(1)
@@ -46,12 +46,12 @@ def parse_proxy_response(text: str) -> tuple[list[str], Optional[str]]:
except (json.JSONDecodeError, ValueError):
pass
if '添加白名单' in text or '白名单' in text:
ip_match = re.search(r'(\d+\.\d+\.\d+\.\d+)', text)
if "添加白名单" in text or "白名单" in text:
ip_match = re.search(r"(\d+\.\d+\.\d+\.\d+)", text)
if ip_match:
return [], ip_match.group(1)
matches = re.findall(r'(\d+\.\d+\.\d+\.\d+):(\d+)', text)
matches = re.findall(r"(\d+\.\d+\.\d+\.\d+):(\d+)", text)
proxies = [f"http://{ip}:{port}" for ip, port in matches]
if proxies:
return proxies, None
+11 -4
View File
@@ -67,7 +67,10 @@ class XiequAdapter(BaseWhitelistAdapter):
# 映射到统一格式
return [
{"ip": r.get("IP", r.get("ip", "")), "memo": r.get("MEMO", r.get("memo", ""))}
{
"ip": r.get("IP", r.get("ip", "")),
"memo": r.get("MEMO", r.get("memo", "")),
}
for r in data
if r.get("IP") or r.get("ip")
]
@@ -97,7 +100,7 @@ class XiequAdapter(BaseWhitelistAdapter):
# 频率限制,等待后重试一次
if retry and ("频率过快" in text or "稍后" in text):
wait = 5
match = re.search(r'(\d+)\s*秒', text)
match = re.search(r"(\d+)\s*秒", text)
if match:
wait = int(match.group(1))
logger.info(f"白名单添加被限流,等待 {wait} 秒后重试...")
@@ -147,9 +150,13 @@ class XiequAdapter(BaseWhitelistAdapter):
try:
records = self.get_whitelist()
count = len(records)
my_records = [r for r in records if r.get("memo", "").startswith(self.MEMO_PREFIX)]
my_records = [
r for r in records if r.get("memo", "").startswith(self.MEMO_PREFIX)
]
msg = f"连接成功,白名单共 {count} 条记录,其中本机相关 {len(my_records)}"
msg = (
f"连接成功,白名单共 {count} 条记录,其中本机相关 {len(my_records)}"
)
logger.info(msg)
return True, msg
+1
View File
@@ -51,6 +51,7 @@ class XkdailiAdapter(BaseWhitelistAdapter):
}
base_params.update(params)
from urllib.parse import urlencode
return f"{self.BASE_URL}?{urlencode(base_params)}"
def _parse_response(self, text: str) -> tuple[bool, str]:
+41 -30
View File
@@ -17,11 +17,9 @@ LogFunc = Callable[[str, str], None]
class WhitelistSyncer(Protocol):
"""代理解析流程需要的白名单能力。"""
def sync_ip(self, ip: str) -> tuple[bool, str]:
...
def sync_ip(self, ip: str) -> tuple[bool, str]: ...
def get_local_exit_ip(self) -> Optional[str]:
...
def get_local_exit_ip(self) -> Optional[str]: ...
class ProxyResolver:
@@ -62,14 +60,16 @@ class ProxyResolver:
return
log_method = getattr(
logger,
level if level in ('debug', 'info', 'warning', 'error', 'success') else 'info',
level
if level in ("debug", "info", "warning", "error", "success")
else "info",
logger.info,
)
log_method(message)
def _sync_ip(self, ip: str) -> tuple[bool, str]:
if not self.whitelist_syncer:
return False, '未配置白名单 UID/UKEY'
return False, "未配置白名单 UID/UKEY"
ok, sync_msg = self.whitelist_syncer.sync_ip(ip)
if ok:
self._last_synced_ip = ip
@@ -82,14 +82,14 @@ class ProxyResolver:
local_ip = self.whitelist_syncer.get_local_exit_ip()
if local_ip and local_ip != self._last_synced_ip:
self._log('info', f"[尝试 {attempt}] 检测出口IP: {local_ip},同步白名单...")
self._log("info", f"[尝试 {attempt}] 检测出口IP: {local_ip},同步白名单...")
ok, sync_msg = self._sync_ip(local_ip)
if ok:
self._log('info', f"白名单同步成功: {sync_msg}")
self._log("info", f"白名单同步成功: {sync_msg}")
else:
self._log('warning', f"白名单同步失败: {sync_msg}")
self._log("warning", f"白名单同步失败: {sync_msg}")
elif local_ip == self._last_synced_ip:
self._log('debug', f"[尝试 {attempt}] 出口IP未变: {local_ip}")
self._log("debug", f"[尝试 {attempt}] 出口IP未变: {local_ip}")
def fetch_verified(
self,
@@ -107,18 +107,18 @@ class ProxyResolver:
for attempt in range(1, max_attempts + 1):
if self._is_stopped():
return None, '任务已停止'
return None, "任务已停止"
if attempt > 1:
delay = min(attempt - 1, 2)
if self.log_func:
self._log('info', f'等待 {delay}s 后重试...')
self._log("info", f"等待 {delay}s 后重试...")
if self._wait_or_stopped(delay):
return None, '任务已停止'
return None, "任务已停止"
self._sync_local_exit_ip_if_needed(attempt)
if self._is_stopped():
return None, '任务已停止'
self._log('info', f'代理预检 {attempt}/{max_attempts}: 正在获取代理')
return None, "任务已停止"
self._log("info", f"代理预检 {attempt}/{max_attempts}: 正在获取代理")
try:
response = requests.get(self.api_url, timeout=10)
@@ -128,38 +128,49 @@ class ProxyResolver:
proxy_urls, whitelist_ip = parse_proxy_response(text)
if proxy_urls:
self._log('info', f'获取到 {len(proxy_urls)} 个代理,并发验证')
available, msg = verify_proxies_concurrent(proxy_urls, return_all=return_all)
self._log("info", f"获取到 {len(proxy_urls)} 个代理,并发验证")
available, msg = verify_proxies_concurrent(
proxy_urls, return_all=return_all
)
if available:
if not return_all:
self._log('success', f'代理预检成功: {available}')
self._log("success", f"代理预检成功: {available}")
return available, msg
last_error = msg
self._log('warning', f"代理预检 {attempt}/{max_attempts}: {last_error}")
self._log(
"warning", f"代理预检 {attempt}/{max_attempts}: {last_error}"
)
continue
if whitelist_ip and self.whitelist_syncer:
if self.sync_whitelist_once and self._has_synced_whitelist:
last_error = f'白名单已同步但代理API仍返回白名单错误: {whitelist_ip}'
self._log('warning', last_error)
last_error = (
f"白名单已同步但代理API仍返回白名单错误: {whitelist_ip}"
)
self._log("warning", last_error)
continue
self._log('warning', f'代理需要白名单IP: {whitelist_ip},自动同步...')
self._log(
"warning", f"代理需要白名单IP: {whitelist_ip},自动同步..."
)
ok, sync_msg = self._sync_ip(whitelist_ip)
self._log('success' if ok else 'error', f'白名单同步: {sync_msg}')
self._log("success" if ok else "error", f"白名单同步: {sync_msg}")
if ok:
self._log('info', '白名单已更新,立即重试...')
self._log("info", "白名单已更新,立即重试...")
continue
return None, f'白名单同步失败: {sync_msg}'
return None, f"白名单同步失败: {sync_msg}"
last_error = '代理API响应无法解析'
self._log('warning', f"代理预检 {attempt}/{max_attempts}: {last_error}: {text[:80]}")
last_error = "代理API响应无法解析"
self._log(
"warning",
f"代理预检 {attempt}/{max_attempts}: {last_error}: {text[:80]}",
)
except Exception as exc:
last_error = f'代理API请求失败: {exc}'
self._log('warning', f"代理预检 {attempt}/{max_attempts}: {last_error}")
last_error = f"代理API请求失败: {exc}"
self._log("warning", f"代理预检 {attempt}/{max_attempts}: {last_error}")
return None, f'代理预检失败({max_attempts}次尝试均失败): {last_error}'
return None, f"代理预检失败({max_attempts}次尝试均失败): {last_error}"
def resolve_working_proxy(
+18 -14
View File
@@ -16,24 +16,24 @@ def verify_proxy_url(proxy_url: str, timeout: tuple = (3, 5)) -> tuple[bool, str
Returns:
(是否可用, 消息)
"""
proxies = {'http': proxy_url, 'https': proxy_url}
proxies = {"http": proxy_url, "https": proxy_url}
# ── 斗鱼主站可达性验证 ──
try:
response = requests.get(
'https://www.douyu.com',
"https://www.douyu.com",
proxies=proxies,
timeout=timeout,
headers={'User-Agent': 'Mozilla/5.0'},
headers={"User-Agent": "Mozilla/5.0"},
)
response.raise_for_status()
return True, '代理可用 → 斗鱼可达'
return True, "代理可用 → 斗鱼可达"
except Exception as exc:
err_msg = str(exc)
if 'Tunnel connection failed' in err_msg or '503' in err_msg:
detail = '代理拒绝连接(白名单可能未生效)'
elif 'timed out' in err_msg.lower():
detail = '连接超时'
if "Tunnel connection failed" in err_msg or "503" in err_msg:
detail = "代理拒绝连接(白名单可能未生效)"
elif "timed out" in err_msg.lower():
detail = "连接超时"
else:
detail = type(exc).__name__
logger.debug(f"代理验证失败 [{proxy_url}]: 斗鱼主站不可达: {detail}")
@@ -54,7 +54,7 @@ def verify_proxies_concurrent(
return_all=True: (可用代理 URL 列表或 None, 消息)
"""
if not proxy_urls:
return None, '无代理可验证'
return None, "无代理可验证"
if len(proxy_urls) == 1:
ok, msg = verify_proxy_url(proxy_urls[0], timeout)
@@ -64,7 +64,9 @@ def verify_proxies_concurrent(
if return_all:
available: list[str] = []
with ThreadPoolExecutor(max_workers=min(max_workers, len(proxy_urls))) as executor:
with ThreadPoolExecutor(
max_workers=min(max_workers, len(proxy_urls))
) as executor:
future_map = {
executor.submit(verify_proxy_url, proxy, timeout): proxy
for proxy in proxy_urls
@@ -77,9 +79,11 @@ def verify_proxies_concurrent(
except Exception:
continue
if available:
logger.success(f"并发验证找到 {len(available)}/{len(proxy_urls)} 个可用代理")
return available, f'找到 {len(available)} 个可用代理'
return None, f'{len(proxy_urls)} 个代理均不可用'
logger.success(
f"并发验证找到 {len(available)}/{len(proxy_urls)} 个可用代理"
)
return available, f"找到 {len(available)} 个可用代理"
return None, f"{len(proxy_urls)} 个代理均不可用"
with ThreadPoolExecutor(max_workers=min(max_workers, len(proxy_urls))) as executor:
future_map = {
@@ -99,4 +103,4 @@ def verify_proxies_concurrent(
except Exception:
continue
return None, f'{len(proxy_urls)} 个代理均不可用'
return None, f"{len(proxy_urls)} 个代理均不可用"
+53 -28
View File
@@ -44,7 +44,8 @@ class FishFinRechargeConfig:
app_key=os.getenv("FISH_FIN_RECHARGE_APP_KEY", "").strip(),
notify_url=os.getenv("FISH_FIN_RECHARGE_NOTIFY_URL", "").strip(),
timeout=(8, timeout),
debug=os.getenv("FISH_FIN_RECHARGE_DEBUG", "false").strip().lower() in {"1", "true", "yes"},
debug=os.getenv("FISH_FIN_RECHARGE_DEBUG", "false").strip().lower()
in {"1", "true", "yes"},
)
def validate(self) -> None:
@@ -86,7 +87,9 @@ class FishFinRechargeClient:
def _sign_value(value: Any) -> str:
"""将参数转为待签名文本;对象按稳定紧凑 JSON 表示。"""
if isinstance(value, (dict, list, tuple)):
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
return json.dumps(
value, ensure_ascii=False, separators=(",", ":"), sort_keys=True
)
if isinstance(value, bool):
return "true" if value else "false"
return str(value)
@@ -143,7 +146,9 @@ class FishFinRechargeClient:
received = str(payload.get("sign") or "").strip().lower()
return bool(received) and received == self.sign(payload, method)
def _request(self, method: str, path: str, params: Mapping[str, Any] | None = None) -> dict[str, Any]:
def _request(
self, method: str, path: str, params: Mapping[str, Any] | None = None
) -> dict[str, Any]:
"""补齐公共参数、签名并执行一次 JSON 请求。"""
request_params: dict[str, Any] = {
"app_id": self.config.app_id,
@@ -167,27 +172,37 @@ class FishFinRechargeClient:
if key != "sign"
},
# 只记录摘要,便于关联排查而不暴露可重放的签名。
"sign_digest": hashlib.sha256(request_params["sign"].encode("utf-8")).hexdigest()[:12],
"sign_digest": hashlib.sha256(
request_params["sign"].encode("utf-8")
).hexdigest()[:12],
}
if self.config.debug:
sign_params = self.normalized_params(request_params)
sign_query = "&".join(f"{key}={sign_params[key]}" for key in sorted(sign_params))
trace_event.update({
"url": url,
"content_type": "application/json",
"json_body": self._debug_payload(request_params),
"sign_params": sign_params,
# 不把可重放签名原文或 AppSecret 写入日志,只记录待签名串摘要。
"sign_source_digest": hashlib.sha256(
f"{sign_query}{method.upper()}".encode("utf-8")
).hexdigest()[:12],
})
sign_query = "&".join(
f"{key}={sign_params[key]}" for key in sorted(sign_params)
)
trace_event.update(
{
"url": url,
"content_type": "application/json",
"json_body": self._debug_payload(request_params),
"sign_params": sign_params,
# 不把可重放签名原文或 AppSecret 写入日志,只记录待签名串摘要。
"sign_source_digest": hashlib.sha256(
f"{sign_query}{method.upper()}".encode("utf-8")
).hexdigest()[:12],
}
)
self.trace(trace_event)
try:
if method.upper() == "GET":
response = self.session.get(url, params=request_params, timeout=self.config.timeout)
response = self.session.get(
url, params=request_params, timeout=self.config.timeout
)
else:
response = self.session.post(url, json=request_params, timeout=self.config.timeout)
response = self.session.post(
url, json=request_params, timeout=self.config.timeout
)
response.raise_for_status()
payload = response.json()
except requests.RequestException as exc:
@@ -205,18 +220,22 @@ class FishFinRechargeClient:
"message": payload.get("msg") or payload.get("message") or "",
"out_order_id": self._response_value(payload, "out_order_id", "outOrderId"),
"order_id": self._response_value(payload, "order_id", "orderId"),
"order_status": self._response_value(payload, "order_status", "orderStatus"),
"order_status": self._response_value(
payload, "order_status", "orderStatus"
),
"fail_reason": self._response_value(payload, "fail_reason", "failReason"),
}
if self.config.debug:
trace_event.update({
"response_headers": {
key: value
for key, value in response.headers.items()
if key.lower() in {"content-type", "x-request-id", "request-id"}
},
"response_body": self._debug_payload(payload),
})
trace_event.update(
{
"response_headers": {
key: value
for key, value in response.headers.items()
if key.lower() in {"content-type", "x-request-id", "request-id"}
},
"response_body": self._debug_payload(payload),
}
)
self.trace(trace_event)
return payload
@@ -263,7 +282,11 @@ class FishFinRechargeClient:
raise ValueError("product_id 不能为空")
if not isinstance(buy_num, int) or isinstance(buy_num, bool) or buy_num < 1:
raise ValueError("buy_num 必须是不小于 1 的整数")
if not isinstance(order_type, int) or isinstance(order_type, bool) or order_type not in {0, 1, 2, 3}:
if (
not isinstance(order_type, int)
or isinstance(order_type, bool)
or order_type not in {0, 1, 2, 3}
):
raise ValueError("order_type 必须是 0 至 3 的整数")
if not isinstance(recharge_arg, list) or not recharge_arg:
raise ValueError("recharge_arg 必须是非空数组")
@@ -287,7 +310,9 @@ class FishFinRechargeClient:
out_order_id = str(out_order_id).strip()
if not out_order_id:
raise ValueError("out_order_id 不能为空")
return self._request("GET", self.QUERY_ORDER_PATH, {"out_order_id": out_order_id})
return self._request(
"GET", self.QUERY_ORDER_PATH, {"out_order_id": out_order_id}
)
def account_info(self) -> dict[str, Any]:
"""查询商户账户信息。"""
+193 -52
View File
@@ -4,46 +4,78 @@ from typing import Any
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5, AES
# 随机产生4个字符组成的字符串
def four_random_chart() -> str:
return hex(int(65536 * (1 + random.random())))[2:][1:]
# PKCS#1 v1.5 填充 + RSA 加密
def parse_jsbn_bigint(n_obj:dict[Any, int]) -> int:
def parse_jsbn_bigint(n_obj: dict[Any, int]) -> int:
DB = 28
DV = 1 << DB
t = n_obj['t']
t = n_obj["t"]
result = 0
for i in range(t):
result += n_obj[i] * (DV ** i)
result += n_obj[i] * (DV**i)
return result
def encrypt_data(plaintext:str) -> str:
def encrypt_data(plaintext: str) -> str:
cipher = PKCS1_v1_5.new(public_key)
encrypted = cipher.encrypt(plaintext.encode('utf-8'))
encrypted = cipher.encrypt(plaintext.encode("utf-8"))
# 转换为十六进制,确保偶数长度
hex_result = encrypted.hex()
if len(hex_result) % 2 == 1:
hex_result = '0' + hex_result
hex_result = "0" + hex_result
return hex_result
def RSA_jiami_r(str_16:str) -> str:
def RSA_jiami_r(str_16: str) -> str:
global public_key
# 你的数据
n_data = {
0: 134982529, 1: 254232810, 2: 164556709, 3: 234907349,
4: 134685994, 5: 35463984, 6: 258277946, 7: 12518857,
8: 44638621, 9: 93783641, 10: 212253739, 11: 62792472,
12: 186688352, 13: 109500232, 14: 182488077, 15: 261196188,
16: 26354094, 17: 103248217, 18: 106891695, 19: 165771045,
20: 41530993, 21: 263704736, 22: 111785174, 23: 12753611,
24: 232116673, 25: 155524985, 26: 218291229, 27: 122452343,
28: 248250238, 29: 118739550, 30: 251169095, 31: 129059733,
32: 149835464, 33: 5498868, 34: 71719731, 35: 154456417,
0: 134982529,
1: 254232810,
2: 164556709,
3: 234907349,
4: 134685994,
5: 35463984,
6: 258277946,
7: 12518857,
8: 44638621,
9: 93783641,
10: 212253739,
11: 62792472,
12: 186688352,
13: 109500232,
14: 182488077,
15: 261196188,
16: 26354094,
17: 103248217,
18: 106891695,
19: 165771045,
20: 41530993,
21: 263704736,
22: 111785174,
23: 12753611,
24: 232116673,
25: 155524985,
26: 218291229,
27: 122452343,
28: 248250238,
29: 118739550,
30: 251169095,
31: 129059733,
32: 149835464,
33: 5498868,
34: 71719731,
35: 154456417,
36: 49635,
't': 37, 's': 0
"t": 37,
"s": 0,
}
e = 65537
@@ -54,11 +86,12 @@ def RSA_jiami_r(str_16:str) -> str:
encrypted = encrypt_data(str_16)
return encrypted
# AES加密
# 加密模式: AES-CBC
# 密钥长度: 128位
# IV: 固定为 "0000000000000000"
def parse_string_to_wordarray(text:str) -> list[int]:
def parse_string_to_wordarray(text: str) -> list[int]:
"""将字符串转换为 WordArray 格式"""
length = len(text)
words = []
@@ -81,13 +114,15 @@ def parse_string_to_wordarray(text:str) -> list[int]:
words[word_index] |= char_code << shift
return words
def AES_O(plaintext:str, str_16:str) -> list[int]:
def AES_O(plaintext: str, str_16: str) -> list[int]:
# 密钥
key_words = parse_string_to_wordarray(str_16)
key = b''.join(w.to_bytes(4, 'big') for w in key_words)
key = b"".join(w.to_bytes(4, "big") for w in key_words)
# IV
iv = b'0000' * 4 # "0000000000000000"
iv = b"0000" * 4 # "0000000000000000"
# 填充(PKCS7)
pad_len = 16 - len(plaintext) % 16
@@ -100,13 +135,14 @@ def AES_O(plaintext:str, str_16:str) -> list[int]:
# 结果是字节数组
return list(ciphertext)
# 自定义base64编码
def geetest_base64_encode(data:list[int]) -> dict[str, Any]:
def geetest_base64_encode(data: list[int]) -> dict[str, Any]:
"""极验自定义Base64编码"""
# 配置
charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()'
pad_char = '.'
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()"
pad_char = "."
# 位掩码 (这些是打乱的)
masks = [7274496, 9483264, 19220, 235]
@@ -159,13 +195,10 @@ def geetest_base64_encode(data:list[int]) -> dict[str, Any]:
break
return {
"res": encoded,
"end": padding
}
return {"res": encoded, "end": padding}
def encrypt_string(e:str, t:list[int], n:str) -> str:
def encrypt_string(e: str, t: list[int], n: str) -> str:
"""
JS加密函数的Python实现
@@ -185,7 +218,7 @@ def encrypt_string(e:str, t:list[int], n:str) -> str:
# 每次读取2个字符(十六进制)
while o < len(n):
r = n[o:o + 2] # 取2个字符
r = n[o : o + 2] # 取2个字符
if len(r) < 2:
break
o += 2
@@ -205,7 +238,7 @@ def encrypt_string(e:str, t:list[int], n:str) -> str:
return i
def simple_md5(message:str) -> str:
def simple_md5(message: str) -> str:
"""
简化版MD5实现,结构更清晰
"""
@@ -216,31 +249,139 @@ def simple_md5(message:str) -> str:
# 轮移位常量
shifts = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
7,
12,
17,
22,
7,
12,
17,
22,
7,
12,
17,
22,
7,
12,
17,
22,
5,
9,
14,
20,
5,
9,
14,
20,
5,
9,
14,
20,
5,
9,
14,
20,
4,
11,
16,
23,
4,
11,
16,
23,
4,
11,
16,
23,
4,
11,
16,
23,
6,
10,
15,
21,
6,
10,
15,
21,
6,
10,
15,
21,
6,
10,
15,
21,
]
# K常数(与JavaScript版本中的常数对应)
K = [
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
0xD76AA478,
0xE8C7B756,
0x242070DB,
0xC1BDCEEE,
0xF57C0FAF,
0x4787C62A,
0xA8304613,
0xFD469501,
0x698098D8,
0x8B44F7AF,
0xFFFF5BB1,
0x895CD7BE,
0x6B901122,
0xFD987193,
0xA679438E,
0x49B40821,
0xF61E2562,
0xC040B340,
0x265E5A51,
0xE9B6C7AA,
0xD62F105D,
0x02441453,
0xD8A1E681,
0xE7D3FBC8,
0x21E1CDE6,
0xC33707D6,
0xF4D50D87,
0x455A14ED,
0xA9E3E905,
0xFCEFA3F8,
0x676F02D9,
0x8D2A4C8A,
0xFFFA3942,
0x8771F681,
0x6D9D6122,
0xFDE5380C,
0xA4BEEA44,
0x4BDECFA9,
0xF6BB4B60,
0xBEBFBC70,
0x289B7EC6,
0xEAA127FA,
0xD4EF3085,
0x04881D05,
0xD9D4D039,
0xE6DB99E5,
0x1FA27CF8,
0xC4AC5665,
0xF4292244,
0x432AFF97,
0xAB9423A7,
0xFC93A039,
0x655B59C3,
0x8F0CCC92,
0xFFEFF47D,
0x85845DD1,
0x6FA87E4F,
0xFE2CE6E0,
0xA3014314,
0x4E0811A1,
0xF7537E82,
0xBD3AF235,
0x2AD7D2BB,
0xEB86D391,
]
# 实际实现...
return verify_result(message)
return verify_result(message)
+60 -8
View File
@@ -11,6 +11,7 @@ REQUEST_TIMEOUT = (3.05, 12)
def pilImgToCv2(img: Image.Image, flag=cv2.COLOR_RGB2BGR):
return cv2.cvtColor(np.asarray(img), flag)
# 识别图片缺口返回滑块距离
def shibie(img: Image.Image, slice: Image.Image):
# 通过 pilImgToCv2 将图片置灰
@@ -38,20 +39,70 @@ def shibie(img: Image.Image, slice: Image.Image):
# showImg(resultBg) # 可以通过它来看处理后的图片效果
return distance
# 还原图片
def restore_geetest_image(input_path:str, output_path:str) -> None:
def restore_geetest_image(input_path: str, output_path: str) -> None:
"""
还原极验打乱的验证码图像
"""
Ut = [
39, 38, 48, 49, 41, 40, 46, 47, 35, 34, 50, 51, 33, 32, 28, 29,
27, 26, 36, 37, 31, 30, 44, 45, 43, 42, 12, 13, 23, 22, 14, 15,
21, 20, 8, 9, 25, 24, 6, 7, 3, 2, 0, 1, 11, 10, 4, 5, 19, 18, 16, 17
39,
38,
48,
49,
41,
40,
46,
47,
35,
34,
50,
51,
33,
32,
28,
29,
27,
26,
36,
37,
31,
30,
44,
45,
43,
42,
12,
13,
23,
22,
14,
15,
21,
20,
8,
9,
25,
24,
6,
7,
3,
2,
0,
1,
11,
10,
4,
5,
19,
18,
16,
17,
]
# 打开混淆图像
img = Image.open(input_path)
new_img = Image.new("RGB", (260,160))
new_img = Image.new("RGB", (260, 160))
r = 160
for _ in range(len(Ut)):
a = r / 2
@@ -63,11 +114,12 @@ def restore_geetest_image(input_path:str, output_path:str) -> None:
new_img.save(output_path)
logger.debug("图像已还原并保存到: {}", output_path)
# 下载图片
def download_picture(bg:str, fullbg:str, slice:str) -> int:
def download_picture(bg: str, fullbg: str, slice: str) -> int:
for i in range(3):
if i == 0:
url = "https://static.geetest.com/"+bg
url = "https://static.geetest.com/" + bg
response = requests.get(url, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
with open("bg.jpg", "wb") as f:
@@ -87,4 +139,4 @@ def download_picture(bg:str, fullbg:str, slice:str) -> int:
response.raise_for_status()
with open("slice.jpg", "wb") as f:
f.write(response.content)
return shibie(Image.open('fullbg.jpg'), Image.open('slice.jpg'))
return shibie(Image.open("fullbg.jpg"), Image.open("slice.jpg"))
+173 -143
View File
@@ -29,13 +29,15 @@ def _get(
def _parse_jsonp_response(response: requests.Response, source: str) -> dict:
"""解析极验 JSONP 响应。"""
response.raise_for_status()
match = re.search(r'\((.*)\)$', response.text)
match = re.search(r"\((.*)\)$", response.text)
if not match:
raise ValueError(f"{source} 无法解析 JSONP 响应")
data = json.loads(match.group(1))
if data.get('status') == 'error':
raise ValueError(f"{source} 失败: {data.get('user_error', data.get('error', '未知错误'))}")
if data.get("status") == "error":
raise ValueError(
f"{source} 失败: {data.get('user_error', data.get('error', '未知错误'))}"
)
return data
@@ -55,26 +57,26 @@ def _parse_json_response(response: requests.Response, source: str) -> dict:
def get_challenge_gt_bak() -> Tuple[str, str]:
headers = {
'accept': 'application/json, text/javascript, */*; q=0.01',
'accept-language': 'zh-CN,zh;q=0.9',
'priority': 'u=1, i',
'referer': 'https://demos.geetest.com/slide-float.html',
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
'x-requested-with': 'XMLHttpRequest',
"accept": "application/json, text/javascript, */*; q=0.01",
"accept-language": "zh-CN,zh;q=0.9",
"priority": "u=1, i",
"referer": "https://demos.geetest.com/slide-float.html",
"sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"x-requested-with": "XMLHttpRequest",
}
params = {
't': str(int(round(time.time() * 1000))),
"t": str(int(round(time.time() * 1000))),
}
response = requests.get(
'https://demos.geetest.com/gt/register-slide',
"https://demos.geetest.com/gt/register-slide",
params=params,
headers=headers,
timeout=10,
@@ -85,21 +87,21 @@ def get_challenge_gt_bak() -> Tuple[str, str]:
def get_challenge_gt() -> Tuple[str, str]:
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Content-Type': 'application/x-www-form-urlencoded',
'referer': 'https://passport.douyu.com/member/login?state=https%3A%2F%2Fwww.douyu.com%2Fmember%2FcpSecurity%2Fcheck_geetest_status',
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Content-Type": "application/x-www-form-urlencoded",
"referer": "https://passport.douyu.com/member/login?state=https%3A%2F%2Fwww.douyu.com%2Fmember%2FcpSecurity%2Fcheck_geetest_status",
}
data = {
'type': '1',
'nicknameOrPhoneEncrypt': '03ILaBwtmmCm0A==',
'password': '57219dddec71c31b7647683fa5306103',
'biz_type': '1',
"type": "1",
"nicknameOrPhoneEncrypt": "03ILaBwtmmCm0A==",
"password": "57219dddec71c31b7647683fa5306103",
"biz_type": "1",
}
response = requests.post(
'https://passport.douyu.com/wgapi/member/passport/login',
"https://passport.douyu.com/wgapi/member/passport/login",
headers=headers,
data=data,
timeout=10,
@@ -114,28 +116,27 @@ def get_challenge_gt() -> Tuple[str, str]:
raise ValueError(f"斗鱼登录接口返回中缺少极验参数: {preview}") from exc
def get_js_address(gt: str, proxies: Optional[Mapping[str, str]] = None) -> dict:
headers = {
'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9',
'referer': PASSPORT_REFERER,
'sec-ch-ua': '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'script',
'sec-fetch-mode': 'no-cors',
'sec-fetch-site': 'cross-site',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"referer": PASSPORT_REFERER,
"sec-ch-ua": '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "script",
"sec-fetch-mode": "no-cors",
"sec-fetch-site": "cross-site",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
}
params = {
'gt': gt,
'callback': 'geetest_' + str(int(round(time.time() * 1000))),
"gt": gt,
"callback": "geetest_" + str(int(round(time.time() * 1000))),
}
response = _get(
'https://api.geetest.com/gettype.php',
"https://api.geetest.com/gettype.php",
params=params,
headers=headers,
proxies=proxies,
@@ -143,6 +144,7 @@ def get_js_address(gt: str, proxies: Optional[Mapping[str, str]] = None) -> dict
return _parse_jsonp_response(response, "极验 gettype")
def get_c_s(
gt: str,
challenge: str,
@@ -150,26 +152,32 @@ def get_c_s(
proxies: Optional[Mapping[str, str]] = None,
) -> Tuple[list[int], str]:
headers = {
'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9',
'referer': PASSPORT_REFERER,
'sec-ch-ua': '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'script',
'sec-fetch-mode': 'no-cors',
'sec-fetch-site': 'cross-site',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"referer": PASSPORT_REFERER,
"sec-ch-ua": '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "script",
"sec-fetch-mode": "no-cors",
"sec-fetch-site": "cross-site",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
}
response = _get(
'https://api.geetest.com/get.php?gt=' + gt + '&challenge=' + challenge + '&lang=zh-cn&pt=0&client_type=web&w=' + w + '&callback=geetest_' + str(
int(round(time.time() * 1000))),
"https://api.geetest.com/get.php?gt="
+ gt
+ "&challenge="
+ challenge
+ "&lang=zh-cn&pt=0&client_type=web&w="
+ w
+ "&callback=geetest_"
+ str(int(round(time.time() * 1000))),
headers=headers,
proxies=proxies,
)
data = _parse_jsonp_response(response, "极验 get.php")
return data['data']['c'], data['data']['s']
return data["data"]["c"], data["data"]["s"]
def req_fullpage_validate(
@@ -180,159 +188,181 @@ def req_fullpage_validate(
) -> dict:
"""HAR 中的 fullpage 最终校验,成功后直接返回 validate。"""
headers = {
'Accept': '*/*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Referer': PASSPORT_REFERER,
'Sec-Fetch-Dest': 'script',
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'cross-site',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
'sec-ch-ua': '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive",
"Referer": PASSPORT_REFERER,
"Sec-Fetch-Dest": "script",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "cross-site",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
"sec-ch-ua": '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
}
response = _get(
'https://api.geetest.com/ajax.php?gt=' + gt + '&challenge=' + challenge + '&lang=zh-cn&pt=0&client_type=web&w=' + w + '&callback=geetest_' + str(
int(round(time.time() * 1000))),
"https://api.geetest.com/ajax.php?gt="
+ gt
+ "&challenge="
+ challenge
+ "&lang=zh-cn&pt=0&client_type=web&w="
+ w
+ "&callback=geetest_"
+ str(int(round(time.time() * 1000))),
headers=headers,
proxies=proxies,
)
return _parse_jsonp_response(response, "极验 fullpage ajax.php")
def req_slide(gt:str, challenge:str, w2:str) -> None:
def req_slide(gt: str, challenge: str, w2: str) -> None:
headers = {
'Accept': '*/*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Referer': 'https://demos.geetest.com/',
'Sec-Fetch-Dest': 'script',
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'cross-site',
'Sec-Fetch-Storage-Access': 'active',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive",
"Referer": "https://demos.geetest.com/",
"Sec-Fetch-Dest": "script",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Storage-Access": "active",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
}
response = requests.get(
'https://api.geevisit.com/ajax.php?gt=' + gt + '&challenge=' + challenge + '&lang=zh-cn&pt=0&client_type=web&w=' + w2 + '&callback=geetest_' + str(
int(round(time.time() * 1000))),
"https://api.geevisit.com/ajax.php?gt="
+ gt
+ "&challenge="
+ challenge
+ "&lang=zh-cn&pt=0&client_type=web&w="
+ w2
+ "&callback=geetest_"
+ str(int(round(time.time() * 1000))),
headers=headers,
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
def get_picture(gt:str, challenge:str) -> tuple[str, str, list[int], str, str, str]:
def get_picture(gt: str, challenge: str) -> tuple[str, str, list[int], str, str, str]:
headers = {
'Accept': '*/*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Referer': 'https://demos.geetest.com/',
'Sec-Fetch-Dest': 'script',
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'cross-site',
'Sec-Fetch-Storage-Access': 'active',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive",
"Referer": "https://demos.geetest.com/",
"Sec-Fetch-Dest": "script",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Storage-Access": "active",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
}
params = {
'is_next': 'true',
'type': 'slide3',
'gt': gt,
'challenge': challenge,
'lang': 'zh-cn',
'https': 'true',
'protocol': 'https://',
'offline': 'false',
'product': 'embed',
'api_server': 'api.geevisit.com',
'isPC': 'true',
'autoReset': 'true',
'width': '100%',
'callback': 'geetest_'+str(int(round(time.time() * 1000))),
"is_next": "true",
"type": "slide3",
"gt": gt,
"challenge": challenge,
"lang": "zh-cn",
"https": "true",
"protocol": "https://",
"offline": "false",
"product": "embed",
"api_server": "api.geevisit.com",
"isPC": "true",
"autoReset": "true",
"width": "100%",
"callback": "geetest_" + str(int(round(time.time() * 1000))),
}
response = requests.get(
'https://api.geevisit.com/get.php',
"https://api.geevisit.com/get.php",
params=params,
headers=headers,
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
logger.debug("极验 get.php 原始响应: {}", response.text[:500])
match = re.search(r'\((.*)\)$', response.text)
match = re.search(r"\((.*)\)$", response.text)
if match:
json_str = match.group(1)
data = json.loads(json_str)
if 'data' in data and isinstance(data['data'], dict):
if "data" in data and isinstance(data["data"], dict):
# 新版极验格式
inner_data = data['data']
inner_data = data["data"]
return (
inner_data.get('bg', ''),
inner_data.get('fullbg', ''),
inner_data.get('c', []),
inner_data.get('s', ''),
inner_data.get('slice', ''),
inner_data.get('challenge', '')
inner_data.get("bg", ""),
inner_data.get("fullbg", ""),
inner_data.get("c", []),
inner_data.get("s", ""),
inner_data.get("slice", ""),
inner_data.get("challenge", ""),
)
else:
# 旧版极验格式
return (
data.get('bg', ''),
data.get('fullbg', ''),
data.get('c', []),
data.get('s', ''),
data.get('slice', ''),
data.get('challenge', '')
data.get("bg", ""),
data.get("fullbg", ""),
data.get("c", []),
data.get("s", ""),
data.get("slice", ""),
data.get("challenge", ""),
)
else:
raise ValueError("无法解析 JSONP 响应")
def req_end(gt:str, challenge:str, w:str) -> dict:
def req_end(gt: str, challenge: str, w: str) -> dict:
headers = {
'Accept': '*/*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Referer': 'https://demos.geetest.com/',
'Sec-Fetch-Dest': 'script',
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'cross-site',
'Sec-Fetch-Storage-Access': 'active',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive",
"Referer": "https://demos.geetest.com/",
"Sec-Fetch-Dest": "script",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Storage-Access": "active",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
}
response = requests.get(
'https://api.geevisit.com/ajax.php?gt='+gt+'&challenge='+challenge+'&lang=zh-cn&%24_BCm=0&client_type=web&w='+w+'&callback=geetest_'+str(int(round(time.time() * 1000))),
"https://api.geevisit.com/ajax.php?gt="
+ gt
+ "&challenge="
+ challenge
+ "&lang=zh-cn&%24_BCm=0&client_type=web&w="
+ w
+ "&callback=geetest_"
+ str(int(round(time.time() * 1000))),
headers=headers,
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
logger.debug("极验 ajax.php 原始响应: {}", response.text[:500])
match = re.search(r'\((.*)\)$', response.text)
match = re.search(r"\((.*)\)$", response.text)
if match:
json_str = match.group(1)
data = json.loads(json_str)
logger.debug("极验验证响应: {}", data)
# 检查验证是否成功
if data.get('success') == 1:
if data.get("success") == 1:
return data
else:
# 如果验证失败,返回错误信息
return {
'success': 0,
'message': data.get('message', '验证失败'),
'validate': ''
"success": 0,
"message": data.get("message", "验证失败"),
"validate": "",
}
else:
raise ValueError("无法解析 JSONP 响应")
+49 -40
View File
@@ -18,63 +18,70 @@ def generate_fake_performance_timing(base_time: Optional[int] = None) -> dict[st
# 定义合理的时间间隔范围(毫秒)
intervals = {
'fetch': random.randint(1, 2),
'domain_lookup_start': random.randint(3, 5),
'domain_lookup': random.randint(5, 15),
'connect': random.randint(50, 150),
'ssl_offset': random.randint(30, 50),
'request': random.randint(1, 5),
'response': random.randint(20, 100),
'response_end': random.randint(1, 3),
'unload_start': random.randint(1, 3),
'unload': random.randint(1, 5),
'dom_loading': random.randint(1, 3),
'dom_interactive': random.randint(50, 200),
'dom_content_loaded': random.randint(1, 3),
'load_event': random.randint(0, 5)
"fetch": random.randint(1, 2),
"domain_lookup_start": random.randint(3, 5),
"domain_lookup": random.randint(5, 15),
"connect": random.randint(50, 150),
"ssl_offset": random.randint(30, 50),
"request": random.randint(1, 5),
"response": random.randint(20, 100),
"response_end": random.randint(1, 3),
"unload_start": random.randint(1, 3),
"unload": random.randint(1, 5),
"dom_loading": random.randint(1, 3),
"dom_interactive": random.randint(50, 200),
"dom_content_loaded": random.randint(1, 3),
"load_event": random.randint(0, 5),
}
timing = {}
# 按照时间顺序构建
timing['navigationStart'] = base_time
timing['fetchStart'] = timing['navigationStart'] + intervals['fetch']
timing['domainLookupStart'] = timing['fetchStart'] + intervals['domain_lookup_start']
timing['domainLookupEnd'] = timing['domainLookupStart'] + intervals['domain_lookup']
timing["navigationStart"] = base_time
timing["fetchStart"] = timing["navigationStart"] + intervals["fetch"]
timing["domainLookupStart"] = (
timing["fetchStart"] + intervals["domain_lookup_start"]
)
timing["domainLookupEnd"] = timing["domainLookupStart"] + intervals["domain_lookup"]
timing['connectStart'] = timing['domainLookupEnd']
timing['secureConnectionStart'] = timing['connectStart'] + intervals['ssl_offset']
timing['connectEnd'] = timing['connectStart'] + intervals['connect']
timing["connectStart"] = timing["domainLookupEnd"]
timing["secureConnectionStart"] = timing["connectStart"] + intervals["ssl_offset"]
timing["connectEnd"] = timing["connectStart"] + intervals["connect"]
timing['requestStart'] = timing['connectEnd'] + intervals['request']
timing['responseStart'] = timing['requestStart'] + intervals['response']
timing['responseEnd'] = timing['responseStart'] + intervals['response_end']
timing["requestStart"] = timing["connectEnd"] + intervals["request"]
timing["responseStart"] = timing["requestStart"] + intervals["response"]
timing["responseEnd"] = timing["responseStart"] + intervals["response_end"]
timing['unloadEventStart'] = timing['responseEnd'] + intervals['unload_start']
timing['unloadEventEnd'] = timing['unloadEventStart'] + intervals['unload']
timing["unloadEventStart"] = timing["responseEnd"] + intervals["unload_start"]
timing["unloadEventEnd"] = timing["unloadEventStart"] + intervals["unload"]
timing['domLoading'] = timing['unloadEventEnd'] + intervals['dom_loading']
timing['domInteractive'] = timing['domLoading'] + intervals['dom_interactive']
timing['domContentLoadedEventStart'] = timing['domInteractive']
timing['domContentLoadedEventEnd'] = timing['domInteractive'] + intervals['dom_content_loaded']
timing['domComplete'] = timing['domContentLoadedEventEnd']
timing['loadEventStart'] = timing['domComplete']
timing['loadEventEnd'] = timing['loadEventStart'] + intervals['load_event']
timing["domLoading"] = timing["unloadEventEnd"] + intervals["dom_loading"]
timing["domInteractive"] = timing["domLoading"] + intervals["dom_interactive"]
timing["domContentLoadedEventStart"] = timing["domInteractive"]
timing["domContentLoadedEventEnd"] = (
timing["domInteractive"] + intervals["dom_content_loaded"]
)
timing["domComplete"] = timing["domContentLoadedEventEnd"]
timing["loadEventStart"] = timing["domComplete"]
timing["loadEventEnd"] = timing["loadEventStart"] + intervals["load_event"]
# 无重定向的情况
timing['redirectStart'] = 0
timing['redirectEnd'] = 0
timing["redirectStart"] = 0
timing["redirectEnd"] = 0
return timing
def __ease_out_expo(sep):
'''
轨迹相关操作
'''
"""
轨迹相关操作
"""
if sep == 1:
return 1
else:
return 1 - pow(2, -10 * sep)
def get_slide_track(distance):
"""
根据滑动距离生成滑动轨迹
@@ -86,7 +93,9 @@ def get_slide_track(distance):
"""
if not isinstance(distance, int) or distance < 0:
raise ValueError(f"distance类型必须是大于等于0的整数: distance: {distance}, type: {type(distance)}")
raise ValueError(
f"distance类型必须是大于等于0的整数: distance: {distance}, type: {type(distance)}"
)
# 初始化轨迹列表
slide_track = [
[random.randint(-50, -10), random.randint(-50, -10), 0],
@@ -110,4 +119,4 @@ def get_slide_track(distance):
slide_track.append([x, _y, t])
_x = x
slide_track.append(slide_track[-1])
return slide_track, slide_track[-1][2]
return slide_track, slide_track[-1][2]
+85 -25
View File
@@ -2,25 +2,60 @@ import time
import random
import json
from loguru import logger
from core.geetest.common.trajectory import generate_realistic_trajectory, process_mouse_trajectory, compress_trajectory, TrajectoryEncoder, \
H
from core.geetest.common.crypto import four_random_chart, RSA_jiami_r, AES_O, geetest_base64_encode, encrypt_string, simple_md5
from core.geetest.common.trajectory import (
generate_realistic_trajectory,
process_mouse_trajectory,
compress_trajectory,
TrajectoryEncoder,
H,
)
from core.geetest.common.crypto import (
four_random_chart,
RSA_jiami_r,
AES_O,
geetest_base64_encode,
encrypt_string,
simple_md5,
)
from core.geetest.common.imaging import download_picture
from core.geetest.common.network import get_challenge_gt, get_js_address, get_c_s, req_slide, get_picture, req_end
from core.geetest.common.performance import generate_fake_performance_timing, get_slide_track
from core.geetest.common.network import (
get_challenge_gt,
get_js_address,
get_c_s,
req_slide,
get_picture,
req_end,
)
from core.geetest.common.performance import (
generate_fake_performance_timing,
get_slide_track,
)
def _generate_seed() -> str:
return four_random_chart() + four_random_chart() + four_random_chart() + four_random_chart()
return (
four_random_chart()
+ four_random_chart()
+ four_random_chart()
+ four_random_chart()
)
def get_w1(gt:str, challenge:str, str_16:str) -> str:
def get_w1(gt: str, challenge: str, str_16: str) -> str:
r = RSA_jiami_r(str_16)
plaintext = '{"gt":"' + gt + '","challenge":"' + challenge + '","offline":false,"new_captcha":true,"product":"float","width":"300px","https":true,"api_server":"apiv6.geetest.com","protocol":"https://","type":"fullpage","static_servers":["static.geetest.com/","static.geevisit.com/"],"voice":"/static/js/voice.1.2.6.js","click":"/static/js/click.3.1.2.js","beeline":"/static/js/beeline.1.0.1.js","fullpage":"/static/js/fullpage.9.2.0-guwyxh.js","slide":"/static/js/slide.7.9.3.js","geetest":"/static/js/geetest.6.0.9.js","aspect_radio":{"slide":103,"click":128,"voice":128,"beeline":50},"cc":16,"ww":true,"i":"-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1"}'
plaintext = (
'{"gt":"'
+ gt
+ '","challenge":"'
+ challenge
+ '","offline":false,"new_captcha":true,"product":"float","width":"300px","https":true,"api_server":"apiv6.geetest.com","protocol":"https://","type":"fullpage","static_servers":["static.geetest.com/","static.geevisit.com/"],"voice":"/static/js/voice.1.2.6.js","click":"/static/js/click.3.1.2.js","beeline":"/static/js/beeline.1.0.1.js","fullpage":"/static/js/fullpage.9.2.0-guwyxh.js","slide":"/static/js/slide.7.9.3.js","geetest":"/static/js/geetest.6.0.9.js","aspect_radio":{"slide":103,"click":128,"voice":128,"beeline":50},"cc":16,"ww":true,"i":"-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1"}'
)
o = AES_O(plaintext, str_16)
i = geetest_base64_encode(o)
return i['res'] + i['end'] + r
return i["res"] + i["end"] + r
def get_w2(gt:str, challenge:str, c:list[int], s:str,str_16:str) -> str:
def get_w2(gt: str, challenge: str, c: list[int], s: str, str_16: str) -> str:
# 伪造浏览器性能数据
fake_timing = generate_fake_performance_timing()
# 映射
@@ -45,7 +80,7 @@ def get_w2(gt:str, challenge:str, c:list[int], s:str,str_16:str) -> str:
"r": fake_timing["domContentLoadedEventEnd"],
"s": fake_timing["domComplete"],
"t": fake_timing["loadEventStart"],
"u": fake_timing["loadEventEnd"]
"u": fake_timing["loadEventEnd"],
}
first_time = int(round(time.time() * 1000)) # 伪造脚本开始运行时间
@@ -55,10 +90,9 @@ def get_w2(gt:str, challenge:str, c:list[int], s:str,str_16:str) -> str:
start_y=random.randint(400, 500),
end_x=853,
end_y=288,
start_time=first_time
start_time=first_time,
)
trajectory = process_mouse_trajectory(guiji_yuanshu_shuzu)["data"]
compressed = compress_trajectory(trajectory)
tt = encrypt_string(compressed, c, s)
@@ -67,14 +101,30 @@ def get_w2(gt:str, challenge:str, c:list[int], s:str,str_16:str) -> str:
rp = simple_md5(gt + challenge + passtime)
plaintext = '{"lang":"zh-cn","type":"fullpage","tt":"'+tt+'","light":"DIV_0","s":"c7c3e21112fe4f741921cb3e4ff9f7cb","h":"321f9af1e098233dbd03f250fd2b5e21","hh":"39bd9cad9e425c3a8f51610fd506e3b3","hi":"09eb21b3ae9542a9bc1e8b63b3d9a467","vip_order":-1,"ct":-1,"ep":{"v":"9.2.0-guwyxh","te":false,"$_BBn":true,"ven":"Google Inc. (AMD)","ren":"ANGLE (AMD, AMD Radeon RX 6750 GRE 12GB (0x000073DF) Direct3D11 vs_5_0 ps_5_0, D3D11)","fp":'+json.dumps(guiji_yuanshu_shuzu[0], separators=(',', ':'))+',"lp":'+json.dumps(guiji_yuanshu_shuzu[-1], separators=(',', ':'))+',"em":{"ph":0,"cp":0,"ek":"11","wd":1,"nt":0,"si":0,"sc":0},"tm":'+json.dumps(web_load_time, separators=(',', ':'))+',"dnf":"dnf","by":0},"passtime":'+passtime+',"rp":"'+rp+'","captcha_token":"112439067","tsfq":"xovrayel"}'
plaintext = (
'{"lang":"zh-cn","type":"fullpage","tt":"'
+ tt
+ '","light":"DIV_0","s":"c7c3e21112fe4f741921cb3e4ff9f7cb","h":"321f9af1e098233dbd03f250fd2b5e21","hh":"39bd9cad9e425c3a8f51610fd506e3b3","hi":"09eb21b3ae9542a9bc1e8b63b3d9a467","vip_order":-1,"ct":-1,"ep":{"v":"9.2.0-guwyxh","te":false,"$_BBn":true,"ven":"Google Inc. (AMD)","ren":"ANGLE (AMD, AMD Radeon RX 6750 GRE 12GB (0x000073DF) Direct3D11 vs_5_0 ps_5_0, D3D11)","fp":'
+ json.dumps(guiji_yuanshu_shuzu[0], separators=(",", ":"))
+ ',"lp":'
+ json.dumps(guiji_yuanshu_shuzu[-1], separators=(",", ":"))
+ ',"em":{"ph":0,"cp":0,"ek":"11","wd":1,"nt":0,"si":0,"sc":0},"tm":'
+ json.dumps(web_load_time, separators=(",", ":"))
+ ',"dnf":"dnf","by":0},"passtime":'
+ passtime
+ ',"rp":"'
+ rp
+ '","captcha_token":"112439067","tsfq":"xovrayel"}'
)
result = geetest_base64_encode(AES_O(plaintext, str_16))
return result['res']+result['end']+result['end']
return result["res"] + result["end"] + result["end"]
def get_w3(str_16:str, challenge:str, hkjl:int, c:list[int], s:str, gt:str) -> str:
def get_w3(
str_16: str, challenge: str, hkjl: int, c: list[int], s: str, gt: str
) -> str:
encoder = TrajectoryEncoder()
u = RSA_jiami_r(str_16)
# 伪造浏览器性能数据
@@ -101,11 +151,9 @@ def get_w3(str_16:str, challenge:str, hkjl:int, c:list[int], s:str, gt:str) -> s
"r": fake_timing["domContentLoadedEventEnd"],
"s": fake_timing["domComplete"],
"t": fake_timing["loadEventStart"],
"u": fake_timing["loadEventEnd"]
"u": fake_timing["loadEventEnd"],
}
trajectory = get_slide_track(hkjl)[0]
logger.debug("滑动轨迹: {}", trajectory)
@@ -117,13 +165,25 @@ def get_w3(str_16:str, challenge:str, hkjl:int, c:list[int], s:str, gt:str) -> s
passtime = str(trajectory[-1][2])
rp = simple_md5(gt + challenge[:32] + passtime)
plaintext = '{"lang":"zh-cn","userresponse":"'+userresponse+'","passtime":'+passtime+',"imgload":50,"aa":"'+aa+'","ep":{"v":"7.9.3","$_BIT":false,"me":true,"tm":'+json.dumps(web_load_time, separators=(',', ':'))+',"td":-1},"h9s9":"1816378497","rp":"'+rp+'"}'
plaintext = (
'{"lang":"zh-cn","userresponse":"'
+ userresponse
+ '","passtime":'
+ passtime
+ ',"imgload":50,"aa":"'
+ aa
+ '","ep":{"v":"7.9.3","$_BIT":false,"me":true,"tm":'
+ json.dumps(web_load_time, separators=(",", ":"))
+ ',"td":-1},"h9s9":"1816378497","rp":"'
+ rp
+ '"}'
)
h = geetest_base64_encode(AES_O(plaintext, str_16))["res"]
return h + u
h = geetest_base64_encode(AES_O(plaintext, str_16))['res']
return h+u
def run_solver() -> None:
# 16位字符串
@@ -154,7 +214,7 @@ def run_solver() -> None:
hkjl = download_picture(bg, fullbg, slice)
# 获取第三个w值
w3 = get_w3(str_16,challenge,hkjl,c,s,gt)
w3 = get_w3(str_16, challenge, hkjl, c, s, gt)
# 最后的验证
message = req_end(gt, challenge, w3)
+29 -13
View File
@@ -26,6 +26,7 @@
python -m core.huya.account_env <账号> <密码> --new-device # 抛弃旧环境, 换新设备
python -m core.huya.account_env <账号> --show # 只查看该账号绑定
"""
from __future__ import annotations
import hashlib
@@ -35,10 +36,10 @@ import sys
import time
from .device_profile import (
HDID32, # 登录帧 t1.t0 32hex 常量 (R15 公式结果, k1 来源见 R39; device_profile.py)
HDID32, # 登录帧 t1.t0 32hex 常量 (R15 公式结果, k1 来源见 R39; device_profile.py)
_load_db,
_save_db,
get_profile, # 幂等画像: 同账号永远复用同一套 (data/huya_device_profiles.json)
get_profile, # 幂等画像: 同账号永远复用同一套 (data/huya_device_profiles.json)
)
from .app_login import HuyaAppPasswordLogin
@@ -77,7 +78,9 @@ def get_or_create_env(account: str, force_new: bool = False) -> dict:
record["guid32"] = _rand_hex(16, b"guid")
changed = True
if "hebe" not in record:
record["hebe"] = {f"Hebe_D{i}": _rand_hex(16, f"hebe{i}".encode()) for i in range(1, 6)}
record["hebe"] = {
f"Hebe_D{i}": _rand_hex(16, f"hebe{i}".encode()) for i in range(1, 6)
}
changed = True
if changed:
db[account] = record
@@ -88,8 +91,12 @@ def get_or_create_env(account: str, force_new: bool = False) -> dict:
# ---------------------------------------------------------------------------
# 绑定 + 登录: 主流程
# ---------------------------------------------------------------------------
def bind_and_login(account: str, password: str,
force_new_device: bool = False, proxies: dict | None = None) -> dict:
def bind_and_login(
account: str,
password: str,
force_new_device: bool = False,
proxies: dict | None = None,
) -> dict:
"""账号 ↔ 环境绑定并登录。
注册链在 login_cred_with_flow 内部执行一次 (register_device 用的 fingerprint
@@ -97,9 +104,11 @@ def bind_and_login(account: str, password: str,
沿用同一组设备字段 (app_login.login_cred_with_flow)。
"""
env = get_or_create_env(account, force_new=force_new_device)
print(f"[env] {account}{env.get('vendor')}/{env.get('model')} "
f"fp40={env.get('fingerprint', '')[:12]}... guid32={env.get('guid32', '')[:12]}... "
f"t1.t0={env.get('hdid', '')[:12]}...")
print(
f"[env] {account}{env.get('vendor')}/{env.get('model')} "
f"fp40={env.get('fingerprint', '')[:12]}... guid32={env.get('guid32', '')[:12]}... "
f"t1.t0={env.get('hdid', '')[:12]}..."
)
# device_info 显式传入本环境 (否则 HuyaAppPasswordLogin 内部会再走一次
# get_profile —— 结果相同, 但显式传入让"环境→注册→登录"的数据流向可读);
@@ -108,13 +117,16 @@ def bind_and_login(account: str, password: str,
login_env.setdefault("device_id", env.get("device_id"))
result = HuyaAppPasswordLogin(
account, password, proxies=proxies, device_info=login_env,
account,
password,
proxies=proxies,
device_info=login_env,
).login()
# ---- 绑定元数据回写 (令牌不入库: t2/t5 每次登录实时签发, 环境才是长期身份) ----
db = _load_db()
record = dict(db.get(account) or env)
record.setdefault("bound_at", int(time.time())) # 首次绑定时间
record.setdefault("bound_at", int(time.time())) # 首次绑定时间
record["last_login"] = {
"ok": result.success,
"msg": result.message[:120],
@@ -125,9 +137,13 @@ def bind_and_login(account: str, password: str,
return {
"account": account,
"env": {"model": env.get("model"), "vendor": env.get("vendor"),
"fingerprint": env.get("fingerprint"), "guid32": env.get("guid32"),
"hdid_t1t0": env.get("hdid")},
"env": {
"model": env.get("model"),
"vendor": env.get("vendor"),
"fingerprint": env.get("fingerprint"),
"guid32": env.get("guid32"),
"hdid_t1t0": env.get("hdid"),
},
"login_success": result.success,
"login_message": result.message,
"code": getattr(result, "code", None),
+6 -2
View File
@@ -245,7 +245,9 @@ class ScoreExchangePrizeResp(TafStruct):
self.msg = ins.read_string(1, default=self.msg)
self.orderId = ins.read_string(3, default=self.orderId)
self.exchangeInfo = ins.read_struct(4, ExchangeInfo) or self.exchangeInfo
self.actPreCondition = ins.read_struct(5, ExchangeActPreCondition) or self.actPreCondition
self.actPreCondition = (
ins.read_struct(5, ExchangeActPreCondition) or self.actPreCondition
)
def write_to(self, os: TafOutputStream):
os.write_int32(0, self.status)
@@ -512,7 +514,9 @@ class ActTaskDetailItem(TafStruct):
@property
def spu_id(self) -> str:
return self._extract_spu_id(self.taskUrl) or self._extract_spu_id(self.taskParams)
return self._extract_spu_id(self.taskUrl) or self._extract_spu_id(
self.taskParams
)
def to_dict(self) -> dict:
return {
+14 -4
View File
@@ -172,7 +172,11 @@ def register_huya_with_sms_line(
if login_result.success and login_result.cookie:
cookie = login_result.cookie
uid = cookie_value(cookie, "udb_uid") or cookie_value(cookie, "yyuid")
username = cookie_value(cookie, "udb_passport") or cookie_value(cookie, "username") or uid
username = (
cookie_value(cookie, "udb_passport")
or cookie_value(cookie, "username")
or uid
)
if not change_password:
return HuyaAutoRegisterResult(
phone=phone,
@@ -207,7 +211,9 @@ def register_huya_with_sms_line(
attempts=attempts,
)
password = fixed_password.strip() or generate_huya_password(password_prefix)
password = fixed_password.strip() or generate_huya_password(
password_prefix
)
change_result = change_huya_password_with_sms_line(
uid=uid,
cookie=cookie,
@@ -220,7 +226,9 @@ def register_huya_with_sms_line(
stop_event=stop_event,
)
if not change_result.success:
failed_status = "stopped" if change_result.message == "已停止" else "error"
failed_status = (
"stopped" if change_result.message == "已停止" else "error"
)
return HuyaAutoRegisterResult(
phone=phone,
provider=item.provider,
@@ -230,7 +238,9 @@ def register_huya_with_sms_line(
cookie=cookie,
code=poll_result.code,
change_code=change_result.code,
sdid=change_result.sdid or login_result.sdid or code_result.sdid,
sdid=change_result.sdid
or login_result.sdid
or code_result.sdid,
normalized_phone=normalized_phone,
username=username,
uid=uid,
+2 -1
View File
@@ -2,6 +2,7 @@
证书格式:base64( [0x0c][key_idx][AES-128-ECB(key16, zeropad(P1))] )
"""
from __future__ import annotations
import base64
@@ -48,7 +49,7 @@ def parse_p1(data: bytes) -> dict:
def tk(n: int) -> bytes:
nonlocal o
b = data[o:o + n]
b = data[o : o + n]
o += n
return b
+12 -6
View File
@@ -29,9 +29,12 @@ FP_STATE_ROOT = Path(__file__).resolve().parents[2] / "data" / "huya_fp_states"
def account_state_dir(account: str) -> Path:
"""账号专属指纹状态目录 (持久化, 保证同一账号多次登录是同一台'设备')。"""
safe = "".join(c if c.isalnum() or c in "-_." else "_" for c in (account or "anon"))[:64]
safe = "".join(
c if c.isalnum() or c in "-_." else "_" for c in (account or "anon")
)[:64]
return FP_STATE_ROOT / safe
DEFAULT_TIMEOUT = (8, 40)
@@ -52,7 +55,9 @@ class HuyaSdidResult:
HDID_PREFIX = "__HDID__"
def _run_node_runner(state_dir: Path, app_id: str, timeout: tuple[float, float]) -> tuple[str, str]:
def _run_node_runner(
state_dir: Path, app_id: str, timeout: tuple[float, float]
) -> tuple[str, str]:
"""调用 node runner,返回 (sdid, hdid)。
若 state_dir/device.json 存在 (账号画像派生的设备覆盖参数), runner 会以该
@@ -79,9 +84,9 @@ def _run_node_runner(state_dir: Path, app_id: str, timeout: tuple[float, float])
for line in (proc.stdout or "").splitlines():
line = line.strip()
if line.startswith(SDID_PREFIX) and len(line) > len(SDID_PREFIX) + 20:
sdid = line[len(SDID_PREFIX):]
sdid = line[len(SDID_PREFIX) :]
if line.startswith(HDID_PREFIX) and len(line) > len(HDID_PREFIX) + 20:
hdid = line[len(HDID_PREFIX):]
hdid = line[len(HDID_PREFIX) :]
if sdid:
return sdid, hdid
stderr_tail = (proc.stderr or "").strip().splitlines()
@@ -168,8 +173,9 @@ def get_huya_sdid(
try:
sdid, hdid = _run_node_runner(state_dir, app_id, timeout)
if sdid:
logger.debug("虎牙设备指纹成功(node): sdid={}... hdid={}...",
sdid[:24], hdid[:10])
logger.debug(
"虎牙设备指纹成功(node): sdid={}... hdid={}...", sdid[:24], hdid[:10]
)
return HuyaSdidResult(sdid=sdid, hdid=hdid, source="fingerprint")
except HuyaFingerprintError as exc:
logger.warning("虎牙 hydevice 指纹失败: {}", exc)
+11 -3
View File
@@ -12,6 +12,7 @@
- 固定: hdid(32hex 硬锚,全账号同一) / app_version / sdk_version
- 动态签发: safedeviceid、登录帧 device_id。
"""
from __future__ import annotations
import hashlib
@@ -85,6 +86,7 @@ def record_login(account: str, ok: bool, message: str = "") -> None:
供 app_login 登录流程调用, GUI 设备绑定页读取展示。
"""
import time as _time
db = _load_db()
rec = db.get(account)
if rec is None:
@@ -112,7 +114,9 @@ def _load_db() -> dict:
def _save_db(db: dict) -> None:
try:
PRIMARY_PROFILE_DB.parent.mkdir(parents=True, exist_ok=True)
PRIMARY_PROFILE_DB.write_text(json.dumps(db, indent=2, ensure_ascii=False), encoding="utf-8")
PRIMARY_PROFILE_DB.write_text(
json.dumps(db, indent=2, ensure_ascii=False), encoding="utf-8"
)
except Exception:
pass
@@ -130,8 +134,12 @@ def _enrich_profile(profile: dict) -> tuple[dict, bool]:
out["guid32"] = hashlib.sha256(os.urandom(16) + b"guid").hexdigest()
changed = True
if len(out.get("hebe") or {}) < 5:
out["hebe"] = {f"Hebe_D{i}": hashlib.sha256(os.urandom(16) + f"hebe{i}".encode()).hexdigest()
for i in range(1, 6)}
out["hebe"] = {
f"Hebe_D{i}": hashlib.sha256(
os.urandom(16) + f"hebe{i}".encode()
).hexdigest()
for i in range(1, 6)
}
changed = True
return out, changed
+17 -6
View File
@@ -2,6 +2,7 @@
基于 XXTEA 算法与 uid + k1 派生密钥。
"""
from __future__ import annotations
import hashlib
@@ -27,13 +28,23 @@ def _xxtea_encrypt_words(v: list[int], k: list[int]) -> list[int]:
p = 0
while p < n - 1:
y = v[p + 1]
z = (v[p] + ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
^ ((s ^ y) + (k[(p & 3) ^ e] ^ z)))) & 0xFFFFFFFF
z = (
v[p]
+ (
(((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
^ ((s ^ y) + (k[(p & 3) ^ e] ^ z))
)
) & 0xFFFFFFFF
v[p] = z
p += 1
y = v[0]
z = (v[n - 1] + ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
^ ((s ^ y) + (k[((n - 1) & 3) ^ e] ^ z)))) & 0xFFFFFFFF
z = (
v[n - 1]
+ (
(((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
^ ((s ^ y) + (k[((n - 1) & 3) ^ e] ^ z))
)
) & 0xFFFFFFFF
v[n - 1] = z
q -= 1
return v
@@ -45,9 +56,9 @@ def xxtea_encrypt(data: bytes, key16: bytes) -> bytes:
nwords = (n // 4) + 1
v = [0] * nwords
for i in range(n // 4):
v[i] = struct.unpack("<I", data[i * 4:i * 4 + 4])[0]
v[i] = struct.unpack("<I", data[i * 4 : i * 4 + 4])[0]
v[nwords - 1] = n
k = [struct.unpack("<I", key16[i * 4:i * 4 + 4])[0] for i in range(4)]
k = [struct.unpack("<I", key16[i * 4 : i * 4 + 4])[0] for i in range(4)]
_xxtea_encrypt_words(v, k)
return b"".join(struct.pack("<I", w & 0xFFFFFFFF) for w in v)
+145 -120
View File
@@ -4,6 +4,7 @@
来源:m-shop.yaoguo.com 的 jce/ShopFacade.js、api/orderui.ts、api/mall/PlayMallNewHome.ts
"""
from typing import List, Dict, Optional
from .taf_protocol import TafOutputStream, TafInputStream, TafStruct, TafType
@@ -78,17 +79,19 @@ def _skip_to_struct_end(ins: TafInputStream):
# 基础结构
# ============================================================
class UserId(TafStruct):
"""用户标识(cookie 在这里)"""
def __init__(self):
self.lUid: int = 0 # tag 0
self.sGuid: str = "" # tag 1
self.sToken: str = "" # tag 2
self.sHuYaUA: str = "" # tag 3 "webh5&0.0.1&websocket&&diypc_52775"
self.sCookie: str = "" # tag 4 完整 cookie
self.iTokenType: int = 0 # tag 5
self.sDeviceInfo: str = "" # tag 6
self.sQIMEI: str = "" # tag 7
self.lUid: int = 0 # tag 0
self.sGuid: str = "" # tag 1
self.sToken: str = "" # tag 2
self.sHuYaUA: str = "" # tag 3 "webh5&0.0.1&websocket&&diypc_52775"
self.sCookie: str = "" # tag 4 完整 cookie
self.iTokenType: int = 0 # tag 5
self.sDeviceInfo: str = "" # tag 6
self.sQIMEI: str = "" # tag 7
def write_to(self, os: TafOutputStream):
# HAR2 实证: 浏览器不优化空串/0值, 全写字段
@@ -114,11 +117,12 @@ class UserId(TafStruct):
class ShopAppInfo(TafStruct):
"""应用信息(HAR2 实证字段顺序: tag0 sAppId, tag1 sBizType, tag4 scene, tag5 sourceId"""
def __init__(self):
self.sAppId: str = "huya" # tag 0
self.sBizType: str = "" # tag 1
self.scene: int = 0 # tag 4
self.sourceId: str = "" # tag 5
self.sAppId: str = "huya" # tag 0
self.sBizType: str = "" # tag 1
self.scene: int = 0 # tag 4
self.sourceId: str = "" # tag 5
def write_to(self, os: TafOutputStream):
# HAR2 实证: 全写字段(含空串/0)
@@ -138,14 +142,16 @@ class ShopAppInfo(TafStruct):
# wsLaunch 初始化
# ============================================================
class WsLaunchSubStruct(TafStruct):
"""wsLaunch tag4 子结构(5个空字符串字段,浏览器强制写)"""
def __init__(self):
self.s0: str = "" # tag 0
self.s1: str = "" # tag 1
self.s2: str = "" # tag 2
self.s3: str = "" # tag 3
self.s4: str = "" # tag 4
self.s0: str = "" # tag 0
self.s1: str = "" # tag 1
self.s2: str = "" # tag 2
self.s3: str = "" # tag 3
self.s4: str = "" # tag 4
def write_to(self, os: TafOutputStream):
# 浏览器写空字符串(STRING1 length0),这里强制写以精确匹配
@@ -169,11 +175,12 @@ class WsLaunchReq(TafStruct):
tag3: appSrc "HUYA&ZH&2052"
tag4: 子struct (5个空字符串)
"""
def __init__(self):
self.lUid: int = 0 # tag 0
self.s1: str = "" # tag 1
self.lUid: int = 0 # tag 0
self.s1: str = "" # tag 1
self.sHuYaUA: str = "webh5&1.0.0&huya" # tag 2
self.appSrc: str = "HUYA&ZH&2052" # tag 3
self.appSrc: str = "HUYA&ZH&2052" # tag 3
self.sub: WsLaunchSubStruct = WsLaunchSubStruct() # tag 4
def write_to(self, os: TafOutputStream):
@@ -192,18 +199,20 @@ class WsLaunchReq(TafStruct):
# 商品查询
# ============================================================
class GetGoodsInfoReqV5(TafStruct):
"""商品查询请求 (shopMiddleUI.getGoodsInfoV5)"""
def __init__(self):
self.userId = UserId() # tag 0
self.userId = UserId() # tag 0
self.shopAppInfo = ShopAppInfo() # tag 1
self.pid: int = 0 # tag 2
self.gameId: str = "" # tag 3
self.spuId: str = "" # tag 4
self.channelStockCode: str = "" # tag 5
self.skuId: int = 0 # tag 6
self.inviterUid: int = 0 # tag 7
self.userModifyPriceId: int = 0 # tag 8
self.pid: int = 0 # tag 2
self.gameId: str = "" # tag 3
self.spuId: str = "" # tag 4
self.channelStockCode: str = "" # tag 5
self.skuId: int = 0 # tag 6
self.inviterUid: int = 0 # tag 7
self.userModifyPriceId: int = 0 # tag 8
def write_to(self, os: TafOutputStream):
# HAR2 实证: 全写字段
@@ -224,13 +233,14 @@ class GetGoodsInfoReqV5(TafStruct):
class GoodsInfoRsp(TafStruct):
"""商品查询响应。"""
def __init__(self):
self.code: int = 0 # tag 0
self.message: str = "" # tag 1
self.goodsInfo = None # tag 2
self.selfGoods: int = 0 # tag 3
self.marketStatus: int = 0 # tag 5
self.timestamp: int = 0 # tag 6
self.code: int = 0 # tag 0
self.message: str = "" # tag 1
self.goodsInfo = None # tag 2
self.selfGoods: int = 0 # tag 3
self.marketStatus: int = 0 # tag 5
self.timestamp: int = 0 # tag 6
def read_from(self, ins: TafInputStream):
self.code = ins.read_int32(0, default=self.code)
@@ -293,6 +303,7 @@ class GoodsInfoRsp(TafStruct):
class GoodsBaseInfo(TafStruct):
"""商品基础信息(getGoodsInfoV5 tag2.tag0)。"""
def __init__(self):
self.spuId: str = ""
self.appId: str = ""
@@ -328,6 +339,7 @@ class GoodsBaseInfo(TafStruct):
class GoodsSkuItem(TafStruct):
"""商品 SKU 信息(getGoodsInfoV5 tag2.tag4.tag3 map value)。"""
def __init__(self):
self.skuId: int = 0
self.spuId: str = ""
@@ -370,6 +382,7 @@ class GoodsSkuItem(TafStruct):
class GoodsPriceInfo(TafStruct):
"""商品价格与 SKU 信息(getGoodsInfoV5 tag2.tag4)。"""
def __init__(self):
self.spuId: str = ""
self.minPrice: int = 0
@@ -435,6 +448,7 @@ class GoodsPriceInfo(TafStruct):
class GoodsInfoDetail(TafStruct):
"""getGoodsInfoV5 响应里的 goodsInfo 主体。"""
def __init__(self):
self.baseInfo = GoodsBaseInfo()
self.priceInfo = GoodsPriceInfo()
@@ -457,8 +471,10 @@ class GoodsInfoDetail(TafStruct):
# 订单历史
# ============================================================
class OrderListShopInfo(TafStruct):
"""订单明细里的店铺信息(只取展示需要的字段)"""
def __init__(self):
self.shopName: str = "" # tag 0
@@ -477,14 +493,15 @@ class OrderListShopInfo(TafStruct):
class OrderListGoodsDetail(TafStruct):
"""订单明细(queryUserOrderList 响应 tag16"""
def __init__(self):
self.spuId: str = "" # tag 4
self.skuId: int = 0 # tag 16
self.buyerUid: int = 0 # tag 18
self.virtualType: int = 0 # tag 19
self.quantity: int = 0 # tag 20
self.spuId: str = "" # tag 4
self.skuId: int = 0 # tag 16
self.buyerUid: int = 0 # tag 18
self.virtualType: int = 0 # tag 19
self.quantity: int = 0 # tag 20
self.shopInfo: Optional[OrderListShopInfo] = None # tag 21
self.points: int = 0 # tag 23
self.points: int = 0 # tag 23
def read_from(self, ins: TafInputStream):
self.spuId = ins.read_string(4, default=self.spuId)
@@ -513,19 +530,20 @@ class OrderListGoodsDetail(TafStruct):
class OrderListItem(TafStruct):
"""订单列表项(queryUserOrderList 响应 tag3 的 list 元素)"""
def __init__(self):
self.bizOrderId: str = "" # tag 0 shop10148750
self.appId: str = "" # tag 1 shop
self.orderId: str = "" # tag 2
self.pid: int = 0 # tag 3
self.shopName: str = "" # tag 4
self.orderStatus: int = 0 # tag 5
self.itemName: str = "" # tag 8
self.unitPrice: int = 0 # tag 9 分
self.quantity: int = 0 # tag 10
self.totalPrice: int = 0 # tag 12 分
self.createTime: int = 0 # tag 14 毫秒时间戳
self.payTime: int = 0 # tag 15 毫秒时间戳
self.bizOrderId: str = "" # tag 0 shop10148750
self.appId: str = "" # tag 1 shop
self.orderId: str = "" # tag 2
self.pid: int = 0 # tag 3
self.shopName: str = "" # tag 4
self.orderStatus: int = 0 # tag 5
self.itemName: str = "" # tag 8
self.unitPrice: int = 0 # tag 9 分
self.quantity: int = 0 # tag 10
self.totalPrice: int = 0 # tag 12 分
self.createTime: int = 0 # tag 14 毫秒时间戳
self.payTime: int = 0 # tag 15 毫秒时间戳
self.goodsDetail: Optional[OrderListGoodsDetail] = None # tag 16
def read_from(self, ins: TafInputStream):
@@ -567,12 +585,13 @@ class OrderListItem(TafStruct):
class QueryUserOrderListReq(TafStruct):
"""查询购买历史订单 (revenueWebUI.queryUserOrderList)"""
def __init__(self):
self.userId = UserId() # tag 0
self.offset: int = 0 # tag 1
self.orderType: int = 1 # tag 2
self.pageSize: int = 10 # tag 3
self.status: int = 0 # tag 4
self.userId = UserId() # tag 0
self.offset: int = 0 # tag 1
self.orderType: int = 1 # tag 2
self.pageSize: int = 10 # tag 3
self.status: int = 0 # tag 4
def write_to(self, os: TafOutputStream):
os.write_struct(0, self.userId)
@@ -587,6 +606,7 @@ class QueryUserOrderListReq(TafStruct):
class QueryUserOrderListRsp(TafStruct):
"""购买历史订单响应"""
def __init__(self):
self.code: int = 0
self.message: str = ""
@@ -624,17 +644,18 @@ class QueryUserOrderListRsp(TafStruct):
# 下单
# ============================================================
class CreateOrderExtraParam(TafStruct):
def __init__(self):
self.freight: int = 0 # tag 0
self.channelStockType: str = "" # tag 1
self.channelStockCode: str = "" # tag 2
self.relatedBizId: str = "" # tag 3
self.bizParams: str = "" # tag 4
self.popupTraceId: str = "" # tag 5
self.supplierUid: int = 0 # tag 6
self.categoryId: str = "" # tag 7
self.ext: str = "" # tag 8
self.freight: int = 0 # tag 0
self.channelStockType: str = "" # tag 1
self.channelStockCode: str = "" # tag 2
self.relatedBizId: str = "" # tag 3
self.bizParams: str = "" # tag 4
self.popupTraceId: str = "" # tag 5
self.supplierUid: int = 0 # tag 6
self.categoryId: str = "" # tag 7
self.ext: str = "" # tag 8
def write_to(self, os: TafOutputStream):
# HAR 实证:下单 extraParam 会强制写默认 0/空串字段
@@ -655,9 +676,9 @@ class CreateOrderExtraParam(TafStruct):
class CreateOrderPromotionParam(TafStruct):
def __init__(self):
self.yxjDeductPrice: int = 0 # tag 0
self.userModifyPriceId: int = 0 # tag 1
self.enablePromotion: int = 1 # tag 2
self.yxjDeductPrice: int = 0 # tag 0
self.userModifyPriceId: int = 0 # tag 1
self.enablePromotion: int = 1 # tag 2
def write_to(self, os: TafOutputStream):
# HAR 实证:tag0/tag1 为 0tag2 为 1
@@ -671,11 +692,11 @@ class CreateOrderPromotionParam(TafStruct):
class CreateOrderAccountParam(TafStruct):
def __init__(self):
self.payoutTypeList: List[int] = [] # tag 0 Vector<INT32>
self.payoutChargeAmount: int = 0 # tag 1
self.cancelPayoutTypeList: List[int] = [] # tag 2
self.recycleSupplierId: int = 0 # tag 3
self.claimPrice: int = 0 # tag 4
self.payoutTypeList: List[int] = [] # tag 0 Vector<INT32>
self.payoutChargeAmount: int = 0 # tag 1
self.cancelPayoutTypeList: List[int] = [] # tag 2
self.recycleSupplierId: int = 0 # tag 3
self.claimPrice: int = 0 # tag 4
def write_to(self, os: TafOutputStream):
# HAR 实证:空 list/0 值也会写出
@@ -691,8 +712,8 @@ class CreateOrderAccountParam(TafStruct):
class PromotionItem(TafStruct):
def __init__(self):
self.promotionId: int = 0 # tag 0
self.promotionType: int = 0 # tag 1
self.promotionId: int = 0 # tag 0
self.promotionType: int = 0 # tag 1
def write_to(self, os: TafOutputStream):
_opt_int(os, 0, self.promotionId)
@@ -705,40 +726,41 @@ class PromotionItem(TafStruct):
class CreateOrderReqV5(TafStruct):
"""下单请求 (shopMiddleUI.createOrderV5)"""
def __init__(self):
self.userId = UserId() # tag 0
self.shopAppInfo = ShopAppInfo() # tag 1
self.receiveId: int = 0 # tag 2
self.pid: int = 0 # tag 3
self.skuId: int = 0 # tag 4
self.itemCount: int = 1 # tag 5
self.remark: str = "" # tag 6
self.spuId: str = "" # tag 7
self.gameId: str = "" # tag 8
self.orderId: int = 0 # tag 9
self.src: int = 0 # tag 10
self.couponUserIds: List[int] = [] # tag 11 Vector<INT64>
self.orderType: int = 0 # tag 12
self.userId = UserId() # tag 0
self.shopAppInfo = ShopAppInfo() # tag 1
self.receiveId: int = 0 # tag 2
self.pid: int = 0 # tag 3
self.skuId: int = 0 # tag 4
self.itemCount: int = 1 # tag 5
self.remark: str = "" # tag 6
self.spuId: str = "" # tag 7
self.gameId: str = "" # tag 8
self.orderId: int = 0 # tag 9
self.src: int = 0 # tag 10
self.couponUserIds: List[int] = [] # tag 11 Vector<INT64>
self.orderType: int = 0 # tag 12
self.extraParam: Optional[CreateOrderExtraParam] = None # tag 13
self.scene: int = 0 # tag 14
self.promotionItems: List = [] # tag 15 Vector<PromotionItem>
self.sourceId: str = "" # tag 16
self.env: Dict[str, str] = {} # tag 17 Map<STRING,STRING>
self.orderScene: int = 0 # tag 18
self.watchWord: str = "" # tag 19
self.marketingChannel: str = "" # tag 20
self.scene: int = 0 # tag 14
self.promotionItems: List = [] # tag 15 Vector<PromotionItem>
self.sourceId: str = "" # tag 16
self.env: Dict[str, str] = {} # tag 17 Map<STRING,STRING>
self.orderScene: int = 0 # tag 18
self.watchWord: str = "" # tag 19
self.marketingChannel: str = "" # tag 20
self.promotionParam: Optional[CreateOrderPromotionParam] = None # tag 21
self.externalTraceKey: str = "" # tag 22
self.kefuUid: int = 0 # tag 23
self.accountParam: Optional[CreateOrderAccountParam] = None # tag 24
self.parentOrderId: int = 0 # tag 25
self.vendorAccountType: str = "" # tag 26
self.vendorAccountVal: str = "" # tag 27
self.vendorSubAccountVal: str = "" # tag 28
self.vendorSubAccountType: str = "" # tag 29
self.bizType: int = 0 # tag 30
self.gameCategoryId: int = 0 # tag 31
self.ext: str = "" # tag 32
self.externalTraceKey: str = "" # tag 22
self.kefuUid: int = 0 # tag 23
self.accountParam: Optional[CreateOrderAccountParam] = None # tag 24
self.parentOrderId: int = 0 # tag 25
self.vendorAccountType: str = "" # tag 26
self.vendorAccountVal: str = "" # tag 27
self.vendorSubAccountVal: str = "" # tag 28
self.vendorSubAccountType: str = "" # tag 29
self.bizType: int = 0 # tag 30
self.gameCategoryId: int = 0 # tag 31
self.ext: str = "" # tag 32
def write_to(self, os: TafOutputStream):
# HAR 实证:createOrderV5 会写出完整字段,即使值为 0/空串/空 list
@@ -782,13 +804,14 @@ class CreateOrderReqV5(TafStruct):
class CreateOrderRsp(TafStruct):
"""下单响应 (shopMiddleUI.createOrderV5)"""
def __init__(self):
self.code: int = 0 # tag 0
self.message: str = "" # tag 1
self.orderId: int = 0 # tag 2 虎牙订单号
self.subOrderId: int = 0 # tag 4
self.orderStatus: int = 0 # tag 5
self.riskUrl: str = "" # tag 8 风控跳转URLcode!=200时可能有)
self.code: int = 0 # tag 0
self.message: str = "" # tag 1
self.orderId: int = 0 # tag 2 虎牙订单号
self.subOrderId: int = 0 # tag 4
self.orderStatus: int = 0 # tag 5
self.riskUrl: str = "" # tag 8 风控跳转URLcode!=200时可能有)
def read_from(self, ins: TafInputStream):
self.code = ins.read_int32(0, default=self.code)
@@ -806,19 +829,21 @@ class CreateOrderRsp(TafStruct):
# 支付(payOrderSubmitV5
# ============================================================
class PayOrderRes(TafStruct):
"""
发起支付响应 (shopMiddleUI.payOrderSubmitV5)
结构从 state_shop_ts.js 的 payOrderRes 推断,tag 顺序按出现顺序
"""
def __init__(self):
self.code: int = 0 # tag 0
self.message: str = "" # tag 1
self.orderId: int = 0 # tag 2 虎牙订单号
self.appOrderId: str = "" # tag 3
self.payOrderId: str = "" # tag 4 支付宝 payOrderId
self.payUrl: str = "" # tag 5 支付宝下单URL(含sign)
self.amount: int = 0 # tag 6
self.code: int = 0 # tag 0
self.message: str = "" # tag 1
self.orderId: int = 0 # tag 2 虎牙订单号
self.appOrderId: str = "" # tag 3
self.payOrderId: str = "" # tag 4 支付宝 payOrderId
self.payUrl: str = "" # tag 5 支付宝下单URL(含sign)
self.amount: int = 0 # tag 6
def read_from(self, ins: TafInputStream):
self.code = ins.read_int32(0, default=self.code)
+69 -52
View File
@@ -8,6 +8,7 @@
0x08 MAP 0x09 LIST 0x0a STRUCT_BEGIN 0x0b STRUCT_END
0x0c ZERO 0x0d SIMPLE_LIST
"""
import struct
import io
from typing import Any, Dict, List, Optional, Tuple
@@ -24,16 +25,17 @@ class TafType:
STRING4 = 0x07
MAP = 0x08
LIST = 0x09
STRUCT_BEGIN = 0x0a
STRUCT_END = 0x0b
ZERO = 0x0c
SIMPLE_LIST = 0x0d
STRUCT_BEGIN = 0x0A
STRUCT_END = 0x0B
ZERO = 0x0C
SIMPLE_LIST = 0x0D
# ============================================================
# 输出流(编码)
# ============================================================
class TafOutputStream:
"""TAF 编码输出流"""
@@ -46,9 +48,9 @@ class TafOutputStream:
# ---- head ----
def write_head(self, tag: int, data_type: int):
if tag < 15:
self.buf.write(struct.pack('B', (tag << 4) | data_type))
self.buf.write(struct.pack("B", (tag << 4) | data_type))
else:
self.buf.write(struct.pack('BB', 0xF0 | data_type, tag))
self.buf.write(struct.pack("BB", 0xF0 | data_type, tag))
# ---- 整数(带自动优化) ----
def write_int8(self, tag: int, value: int):
@@ -56,28 +58,28 @@ class TafOutputStream:
self.write_head(tag, TafType.ZERO)
else:
self.write_head(tag, TafType.INT8)
self.buf.write(struct.pack('b', value))
self.buf.write(struct.pack("b", value))
def write_int16(self, tag: int, value: int):
if -128 <= value <= 127:
self.write_int8(tag, value)
else:
self.write_head(tag, TafType.INT16)
self.buf.write(struct.pack('>h', value))
self.buf.write(struct.pack(">h", value))
def write_int32(self, tag: int, value: int):
if -32768 <= value <= 32767:
self.write_int16(tag, value)
else:
self.write_head(tag, TafType.INT32)
self.buf.write(struct.pack('>i', value))
self.buf.write(struct.pack(">i", value))
def write_int64(self, tag: int, value: int):
if -2147483648 <= value <= 2147483647:
self.write_int32(tag, value)
else:
self.write_head(tag, TafType.INT64)
self.buf.write(struct.pack('>q', value))
self.buf.write(struct.pack(">q", value))
def write_uint64(self, tag: int, value: int):
"""uint64:超过 int32 范围用 INT64"""
@@ -85,27 +87,27 @@ class TafOutputStream:
self.write_int32(tag, value)
else:
self.write_head(tag, TafType.INT64)
self.buf.write(struct.pack('>Q', value))
self.buf.write(struct.pack(">Q", value))
# ---- 浮点 ----
def write_float(self, tag: int, value: float):
self.write_head(tag, TafType.FLOAT)
self.buf.write(struct.pack('>f', value))
self.buf.write(struct.pack(">f", value))
def write_double(self, tag: int, value: float):
self.write_head(tag, TafType.DOUBLE)
self.buf.write(struct.pack('>d', value))
self.buf.write(struct.pack(">d", value))
# ---- 字符串 ----
def write_string(self, tag: int, value: str):
encoded = value.encode('utf-8')
encoded = value.encode("utf-8")
length = len(encoded)
if length > 255:
self.write_head(tag, TafType.STRING4)
self.buf.write(struct.pack('>I', length))
self.buf.write(struct.pack(">I", length))
else:
self.write_head(tag, TafType.STRING1)
self.buf.write(struct.pack('B', length))
self.buf.write(struct.pack("B", length))
self.buf.write(encoded)
# ---- 字节数组 ----
@@ -133,8 +135,9 @@ class TafOutputStream:
self.write_struct_end()
# ---- Map ----
def write_map(self, tag: int, value: Dict[Any, Any],
key_writer=None, val_writer=None):
def write_map(
self, tag: int, value: Dict[Any, Any], key_writer=None, val_writer=None
):
self.write_head(tag, TafType.MAP)
self.write_int32(0, len(value))
for k, v in value.items():
@@ -172,7 +175,7 @@ class TafOutputStream:
self.write_map(tag, value)
elif isinstance(value, (list, tuple)):
self.write_list(tag, list(value))
elif hasattr(value, 'write_to'):
elif hasattr(value, "write_to"):
self.write_struct(tag, value)
else:
raise TypeError(f"不支持的类型: {type(value)}")
@@ -182,6 +185,7 @@ class TafOutputStream:
# 输入流(解码)—— 完整实现,支持所有类型
# ============================================================
class TafInputStream:
"""TAF 解码输入流"""
@@ -201,14 +205,14 @@ class TafInputStream:
data = self.buf.read(1)
if not data:
raise EOFError("读取到文件末尾")
b = struct.unpack('B', data)[0]
b = struct.unpack("B", data)[0]
tag = (b >> 4) & 0x0F
data_type = b & 0x0F
if tag == 15:
data = self.buf.read(1)
if not data:
raise EOFError("读取 tag 扩展字节失败")
tag = struct.unpack('B', data)[0]
tag = struct.unpack("B", data)[0]
return tag, data_type
# ---- 跳过 ----
@@ -228,10 +232,10 @@ class TafInputStream:
elif data_type == TafType.DOUBLE:
self.buf.read(8)
elif data_type == TafType.STRING1:
length = struct.unpack('B', self.buf.read(1))[0]
length = struct.unpack("B", self.buf.read(1))[0]
self.buf.read(length)
elif data_type == TafType.STRING4:
length = struct.unpack('>I', self.buf.read(4))[0]
length = struct.unpack(">I", self.buf.read(4))[0]
self.buf.read(length)
elif data_type == TafType.MAP:
self._skip_map()
@@ -258,13 +262,13 @@ class TafInputStream:
if dtype == TafType.ZERO:
return 0
if dtype == TafType.INT8:
return struct.unpack('b', self.buf.read(1))[0]
return struct.unpack("b", self.buf.read(1))[0]
if dtype == TafType.INT16:
return struct.unpack('>h', self.buf.read(2))[0]
return struct.unpack(">h", self.buf.read(2))[0]
if dtype == TafType.INT32:
return struct.unpack('>i', self.buf.read(4))[0]
return struct.unpack(">i", self.buf.read(4))[0]
if dtype == TafType.INT64:
return struct.unpack('>q', self.buf.read(8))[0]
return struct.unpack(">q", self.buf.read(8))[0]
raise ValueError(f"期望整数, 实际 0x{dtype:02x}")
def _skip_struct(self):
@@ -335,19 +339,23 @@ class TafInputStream:
if dtype == TafType.ZERO:
return 0
if dtype == TafType.INT8:
return struct.unpack('B', self.buf.read(1))[0]
return struct.unpack("B", self.buf.read(1))[0]
if dtype == TafType.INT16:
return struct.unpack('>H', self.buf.read(2))[0]
return struct.unpack(">H", self.buf.read(2))[0]
if dtype == TafType.INT32:
return struct.unpack('>I', self.buf.read(4))[0]
return struct.unpack(">I", self.buf.read(4))[0]
if dtype == TafType.INT64:
return struct.unpack('>Q', self.buf.read(8))[0]
return struct.unpack(">Q", self.buf.read(8))[0]
raise ValueError(f"期望 uint, 实际 0x{dtype:02x}")
def read_boolean(self, tag: int, required: bool = False, default: bool = False) -> bool:
def read_boolean(
self, tag: int, required: bool = False, default: bool = False
) -> bool:
return bool(self.read_int8(tag, required, 1 if default else 0))
def read_float(self, tag: int, required: bool = False, default: float = 0.0) -> float:
def read_float(
self, tag: int, required: bool = False, default: float = 0.0
) -> float:
found = self._find_tag(tag, required)
if not found:
return default
@@ -355,12 +363,14 @@ class TafInputStream:
if dtype == TafType.ZERO:
return 0.0
if dtype == TafType.FLOAT:
return struct.unpack('>f', self.buf.read(4))[0]
return struct.unpack(">f", self.buf.read(4))[0]
if dtype == TafType.DOUBLE:
return struct.unpack('>d', self.buf.read(8))[0]
return struct.unpack(">d", self.buf.read(8))[0]
return float(self._read_int_value(dtype))
def read_double(self, tag: int, required: bool = False, default: float = 0.0) -> float:
def read_double(
self, tag: int, required: bool = False, default: float = 0.0
) -> float:
return self.read_float(tag, required, default)
def read_string(self, tag: int, required: bool = False, default: str = "") -> str:
@@ -369,14 +379,16 @@ class TafInputStream:
return default
dtype = found[1]
if dtype == TafType.STRING1:
length = struct.unpack('B', self.buf.read(1))[0]
length = struct.unpack("B", self.buf.read(1))[0]
elif dtype == TafType.STRING4:
length = struct.unpack('>I', self.buf.read(4))[0]
length = struct.unpack(">I", self.buf.read(4))[0]
else:
raise ValueError(f"期望 string, 实际 0x{dtype:02x}")
return self.buf.read(length).decode('utf-8', errors='replace')
return self.buf.read(length).decode("utf-8", errors="replace")
def read_bytes(self, tag: int, required: bool = False, default: bytes = b'') -> bytes:
def read_bytes(
self, tag: int, required: bool = False, default: bytes = b""
) -> bytes:
found = self._find_tag(tag, required)
if not found:
return default
@@ -388,8 +400,9 @@ class TafInputStream:
return self.buf.read(length)
# ---- 复合类型 ----
def read_map(self, tag: int, required: bool = False,
key_reader=None, val_reader=None) -> Dict:
def read_map(
self, tag: int, required: bool = False, key_reader=None, val_reader=None
) -> Dict:
found = self._find_tag(tag, required)
if not found:
return {}
@@ -405,8 +418,7 @@ class TafInputStream:
result[k] = v
return result
def read_list(self, tag: int, required: bool = False,
item_reader=None) -> List:
def read_list(self, tag: int, required: bool = False, item_reader=None) -> List:
found = self._find_tag(tag, required)
if not found:
return []
@@ -439,13 +451,18 @@ class TafInputStream:
return reader(self, 0)
# 自动推断
if dtype == TafType.STRING1:
length = struct.unpack('B', self.buf.read(1))[0]
return self.buf.read(length).decode('utf-8', errors='replace')
length = struct.unpack("B", self.buf.read(1))[0]
return self.buf.read(length).decode("utf-8", errors="replace")
if dtype == TafType.STRING4:
length = struct.unpack('>I', self.buf.read(4))[0]
return self.buf.read(length).decode('utf-8', errors='replace')
if dtype in (TafType.ZERO, TafType.INT8, TafType.INT16,
TafType.INT32, TafType.INT64):
length = struct.unpack(">I", self.buf.read(4))[0]
return self.buf.read(length).decode("utf-8", errors="replace")
if dtype in (
TafType.ZERO,
TafType.INT8,
TafType.INT16,
TafType.INT32,
TafType.INT64,
):
return self._read_int_value(dtype)
if dtype == TafType.STRUCT_BEGIN:
# 未知 struct,跳过
@@ -459,6 +476,7 @@ class TafInputStream:
# 结构体基类
# ============================================================
class TafStruct:
"""TAF 结构体基类:子类实现 write_to / read_from"""
@@ -470,8 +488,7 @@ class TafStruct:
def to_dict(self) -> Dict[str, Any]:
"""调试用:转字典"""
return {k: v for k, v in self.__dict__.items()
if not k.startswith('_')}
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
def __repr__(self):
return f"{self.__class__.__name__}({self.to_dict()})"
+1
View File
@@ -2,6 +2,7 @@
基于 AES-128-ECB 与 0 填充。
"""
from __future__ import annotations
from Crypto.Cipher import AES
+37 -13
View File
@@ -134,12 +134,18 @@ def make_user_action(now_ms: int | None = None) -> str:
"longitude": "-1.0",
"ssid": "",
"user_action": [
{"id": "24", "time": str(t1),
"x": str(random.randint(150, 900)),
"y": str(random.randint(800, 1600))},
{"id": "11", "time": str(t2),
"x": str(random.randint(150, 900)),
"y": str(random.randint(800, 1600))},
{
"id": "24",
"time": str(t1),
"x": str(random.randint(150, 900)),
"y": str(random.randint(800, 1600)),
},
{
"id": "11",
"time": str(t2),
"x": str(random.randint(150, 900)),
"y": str(random.randint(800, 1600)),
},
],
},
ensure_ascii=False,
@@ -152,10 +158,17 @@ def make_trace_id(pid: int = 0) -> str:
return f"{random.getrandbits(64):016x}-{pid}-{int(_time.time() * 1000)}"
def _build_wup_data(w: _Writer, uid_str: str, sha1_password: str,
safedeviceid: str, hdid: str, session: int,
trace_id: str, user_action_json: str,
device_info: Dict[str, str]) -> None:
def _build_wup_data(
w: _Writer,
uid_str: str,
sha1_password: str,
safedeviceid: str,
hdid: str,
session: int,
trace_id: str,
user_action_json: str,
device_info: Dict[str, str],
) -> None:
"""编码 _wup_data struct。"""
meta_json = _build_meta_json(session, trace_id)
name = _make_name(uid_str)
@@ -179,7 +192,9 @@ def _build_wup_data(w: _Writer, uid_str: str, sha1_password: str,
# -- t1: 设备信息 struct --
di = device_info
w.struct_begin(1)
w.string(0, hdid) # t1.t0 = HDID32 (登录帧32hex设备证书, libhydeviceid.so 硬锚, 换→APP_SIGN_NOT_MATCH)
w.string(
0, hdid
) # t1.t0 = HDID32 (登录帧32hex设备证书, libhydeviceid.so 硬锚, 换→APP_SIGN_NOT_MATCH)
w.string(1, di.get("app_version", "13.4.22"))
w.string(2, di.get("sdk_version", "1.0.80138"))
w.string(3, "")
@@ -224,8 +239,17 @@ def build_password_login_wup(
) -> bytes:
"""构造密码登录的 WUP TAF 请求体。"""
wd = _Writer()
_build_wup_data(wd, uid_str, sha1_password, safedeviceid, hdid,
session, trace_id, user_action_json, device_info)
_build_wup_data(
wd,
uid_str,
sha1_password,
safedeviceid,
hdid,
session,
trace_id,
user_action_json,
device_info,
)
wup_data = wd.get()
req = _Writer()
+26 -20
View File
@@ -9,6 +9,7 @@ Wup 包结构:
tag7:sBuffer(bytes), tag8:iTimeout, tag9:context(map), tag10:status(map)
sBuffer = Map<"tReq", 编码后的请求结构体>
"""
import struct
from typing import Any, Dict, Optional
from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct
@@ -18,16 +19,16 @@ class WupRequest:
"""Wup 请求对象"""
def __init__(self):
self.iVersion: int = 3 # tag 1
self.cPacketType: int = 0 # tag 2
self.iMessageType: int = 0 # tag 3
self.iRequestId: int = 0 # tag 4
self.sServantName: str = "" # tag 5
self.sFuncName: str = "" # tag 6
self.sBuffer: bytes = b'' # tag 7
self.iTimeout: int = 3000 # tag 8
self.context: Dict[str, str] = {} # tag 9
self.status: Dict[str, str] = {} # tag 10
self.iVersion: int = 3 # tag 1
self.cPacketType: int = 0 # tag 2
self.iMessageType: int = 0 # tag 3
self.iRequestId: int = 0 # tag 4
self.sServantName: str = "" # tag 5
self.sFuncName: str = "" # tag 6
self.sBuffer: bytes = b"" # tag 7
self.iTimeout: int = 3000 # tag 8
self.context: Dict[str, str] = {} # tag 9
self.status: Dict[str, str] = {} # tag 10
self.newdata: Dict[str, bytes] = {}
def setServant(self, name: str):
@@ -49,7 +50,7 @@ class WupRequest:
"""
os = TafOutputStream()
if isinstance(struct_data, TafStruct) or hasattr(struct_data, 'write_to'):
if isinstance(struct_data, TafStruct) or hasattr(struct_data, "write_to"):
# 结构体对象:STRUCT_BEGIN + 内容 + STRUCT_END
os.write_struct(0, struct_data)
elif isinstance(struct_data, dict):
@@ -83,7 +84,7 @@ class WupRequest:
os.write_map(tag, value)
elif isinstance(value, (list, tuple)):
os.write_list(tag, list(value))
elif hasattr(value, 'write_to'):
elif hasattr(value, "write_to"):
os.write_struct(tag, value)
else:
raise TypeError(f"不支持的字段类型: {type(value)}")
@@ -115,14 +116,14 @@ class WupRequest:
# 3. 长度前缀
length = 4 + len(wup_body)
return struct.pack('>I', length) + wup_body
return struct.pack(">I", length) + wup_body
def normalize_wup_payload(data: bytes) -> bytes:
"""去掉可选的 4 字节 WUP 长度前缀,返回裸 WUP body"""
if len(data) < 4:
return data
declared_len = struct.unpack('>I', data[0:4])[0]
declared_len = struct.unpack(">I", data[0:4])[0]
if declared_len == len(data) or declared_len + 4 == len(data):
return data[4:]
return data
@@ -138,7 +139,7 @@ class WupResponse:
self.iRequestId: int = 0
self.sServantName: str = ""
self.sFuncName: str = ""
self.sBuffer: bytes = b''
self.sBuffer: bytes = b""
self.iTimeout: int = 0
self.context: Dict[str, str] = {}
self.status: Dict[str, str] = {}
@@ -249,19 +250,20 @@ class WupResponse:
# 辅助:按已知 dtype 读取值
# ============================================================
def _read_string_value(ins: TafInputStream, dtype: int) -> str:
if dtype == TafType.STRING1:
length = struct.unpack('B', ins.buf.read(1))[0]
length = struct.unpack("B", ins.buf.read(1))[0]
elif dtype == TafType.STRING4:
length = struct.unpack('>I', ins.buf.read(4))[0]
length = struct.unpack(">I", ins.buf.read(4))[0]
else:
return ""
return ins.buf.read(length).decode('utf-8', errors='replace')
return ins.buf.read(length).decode("utf-8", errors="replace")
def _read_bytes_value(ins: TafInputStream, dtype: int) -> bytes:
if dtype != TafType.SIMPLE_LIST:
return b''
return b""
ins.read_head() # 元素类型 INT8
length = ins._read_int_len()
return ins.buf.read(length)
@@ -276,6 +278,10 @@ def _read_map_value(ins: TafInputStream, dtype: int) -> Dict:
_, kt = ins.read_head()
k = _read_string_value(ins, kt)
_, vt = ins.read_head()
v = _read_string_value(ins, vt) if vt in (TafType.STRING1, TafType.STRING4) else ""
v = (
_read_string_value(ins, vt)
if vt in (TafType.STRING1, TafType.STRING4)
else ""
)
result[k] = v
return result
+35 -15
View File
@@ -58,12 +58,14 @@ def parse_sms_lines(text: str) -> list[SmsLine]:
continue
if not phone or not url:
continue
rows.append(SmsLine(
phone=phone,
url=url,
provider=detect_provider(url),
raw=line,
))
rows.append(
SmsLine(
phone=phone,
url=url,
provider=detect_provider(url),
raw=line,
)
)
return rows
@@ -133,32 +135,50 @@ class SmsProviderClient:
if low.startswith("yes|"):
code = extract_sms_code(body)
if code:
return SmsPollResult(status="code", code=code, message="收到验证码", raw=body)
return SmsPollResult(status="error", message=f"收到短信但未识别验证码: {body}", raw=body)
return SmsPollResult(
status="code", code=code, message="收到验证码", raw=body
)
return SmsPollResult(
status="error", message=f"已收到短信但未识别验证码: {body}", raw=body
)
code = extract_sms_code(body)
if code:
return SmsPollResult(status="code", code=code, message="收到验证码", raw=body)
return SmsPollResult(
status="code", code=code, message="收到验证码", raw=body
)
return SmsPollResult(status="waiting", message=body, raw=body)
def _parse_sms8(self, body: str, after_time: datetime | None = None) -> SmsPollResult:
def _parse_sms8(
self, body: str, after_time: datetime | None = None
) -> SmsPollResult:
try:
payload = json.loads(body)
except json.JSONDecodeError:
code = extract_sms_code(body)
if code:
return SmsPollResult(status="code", code=code, message="收到验证码", raw=body)
return SmsPollResult(status="error", message=f"短信平台返回非 JSON: {body[:120]}", raw=body)
return SmsPollResult(
status="code", code=code, message="收到验证码", raw=body
)
return SmsPollResult(
status="error", message=f"短信平台返回非 JSON: {body[:120]}", raw=body
)
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
msg = str(payload.get("msg") or "")
code_text = str(data.get("code") or "")
code_time = _parse_sms8_time(str(data.get("code_time") or ""))
if after_time and code_time and code_time < after_time.replace(tzinfo=None):
return SmsPollResult(status="waiting", message="验证码时间早于本次发码", raw=body)
return SmsPollResult(
status="waiting", message="验证码时间早于本次发码", raw=body
)
code = extract_sms_code(code_text)
if int(payload.get("code") or 0) == 1 and code:
return SmsPollResult(status="code", code=code, message="收到验证码", raw=body)
return SmsPollResult(
status="code", code=code, message="收到验证码", raw=body
)
if code:
return SmsPollResult(status="code", code=code, message="收到验证码", raw=body)
return SmsPollResult(
status="code", code=code, message="收到验证码", raw=body
)
return SmsPollResult(status="waiting", message=msg or "暂无验证码", raw=body)