style: 统一 Ruff 代码格式
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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|jordan(requests 会自动 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,
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 响应")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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 为 0,tag2 为 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 风控跳转URL(code!=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 风控跳转URL(code!=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
@@ -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()})"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
基于 AES-128-ECB 与 0 填充。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
+37
-13
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user