v0.2.7: 修复Redis连接 + 启动管理后台
- 修复Redis认证 (配置密码) - 启动Python管理后台 (端口9531, 15个功能开关) - 统一版本号 0.2.7 - 更新docker-compose.yml (镜像版本/Redis URL/Admin服务)
This commit is contained in:
339
cloudsearch_transfer/adapter/xunlei/credential.py
Normal file
339
cloudsearch_transfer/adapter/xunlei/credential.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
CloudSearch Transfer — 迅雷网盘凭证管理器 v1.0.0
|
||||
|
||||
迅雷网盘使用 refresh_token + captcha_token 双重认证机制:
|
||||
|
||||
1. refresh_token → access_token (OAuth)
|
||||
POST https://xluser-ssl.xunlei.com/v1/auth/token
|
||||
Body: {"grant_type": "refresh_token", "refresh_token": "...", "client_id": "..."}
|
||||
|
||||
2. captcha_token 获取(某些操作需要)
|
||||
POST /v1/shield/captcha/init
|
||||
Body: {"client_id": "...", "action": "...", "device_id": "...", "meta": {"captcha_sign": "..."}}
|
||||
|
||||
3. get_headers() 返回所有需要的认证头:
|
||||
Authorization: Bearer <access_token>
|
||||
x-captcha-token: <captcha_token>
|
||||
x-client-id: <client_id>
|
||||
x-device-id: <device_id>
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
import threading
|
||||
from typing import Dict, Optional
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ─── 常量 ───────────────────────────────────────────────────────────
|
||||
# 迅雷网盘 OAuth 认证端点
|
||||
XUNLEI_AUTH_API = "https://xluser-ssl.xunlei.com"
|
||||
|
||||
# 迅雷网盘客户端标识(固定值)
|
||||
CLIENT_ID = "Xqp0kJBXWhwaTpB6"
|
||||
DEVICE_ID = "925b7631473a13716b791d7f28289cad"
|
||||
|
||||
# ─── 默认请求头 ─────────────────────────────────────────────────────
|
||||
DEFAULT_HEADERS: Dict[str, str] = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/135.0.0.0 Safari/537.36"
|
||||
),
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
class XunleiCredentialManager:
|
||||
"""迅雷网盘凭证管理器。
|
||||
|
||||
职责:
|
||||
- 使用 refresh_token 换取 access_token
|
||||
- 获取 captcha_token(特定 action 需要)
|
||||
- 构建包含所有认证头的请求头字典
|
||||
- 访问令牌过期前自动刷新(提前 60s)
|
||||
|
||||
用法:
|
||||
mgr = XunleiCredentialManager(refresh_token="xxx")
|
||||
mgr.refresh_access_token() # 刷新 access_token
|
||||
captcha = mgr.get_captcha_token("restore") # 获取验证码令牌
|
||||
headers = mgr.get_headers() # 获取完整的认证请求头
|
||||
is_ok = mgr.validate() # 验证凭证有效性
|
||||
|
||||
Attributes:
|
||||
CLIENT_ID: 迅雷客户端 ID。
|
||||
DEVICE_ID: 设备标识。
|
||||
"""
|
||||
|
||||
# ─── 类常量 ────────────────────────────────────────────────
|
||||
CLIENT_ID: str = CLIENT_ID
|
||||
DEVICE_ID: str = DEVICE_ID
|
||||
|
||||
def __init__(self, refresh_token: str = "") -> None:
|
||||
"""初始化迅雷凭证管理器。
|
||||
|
||||
Args:
|
||||
refresh_token: 迅雷网盘的 refresh_token。
|
||||
"""
|
||||
self._refresh_token: str = refresh_token.strip()
|
||||
self._access_token: str = ""
|
||||
self._expires_at: float = 0.0
|
||||
self._captcha_tokens: Dict[str, str] = {} # action → captcha_token
|
||||
self._lock: threading.Lock = threading.Lock()
|
||||
self._session: requests.Session = requests.Session()
|
||||
self._session.headers.update(DEFAULT_HEADERS)
|
||||
|
||||
# ─── 公开 API ──────────────────────────────────────────────
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""验证 refresh_token 是否有效。
|
||||
|
||||
要求 refresh_token 长度 >= 20,且能成功换取 access_token。
|
||||
|
||||
Returns:
|
||||
True 表示凭证有效。
|
||||
"""
|
||||
if not self._refresh_token or len(self._refresh_token) < 20:
|
||||
logger.warning(
|
||||
"[XunleiCredential] refresh_token 长度不足 20,验证失败"
|
||||
)
|
||||
return False
|
||||
return self.refresh_access_token()
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""validate() 的别名。"""
|
||||
return self.validate()
|
||||
|
||||
def refresh_access_token(self) -> bool:
|
||||
"""使用 refresh_token 换取 access_token。
|
||||
|
||||
POST /v1/auth/token
|
||||
Body: {"grant_type": "refresh_token", "refresh_token": "...", "client_id": "..."}
|
||||
|
||||
返回 True 表示成功,False 表示失败。
|
||||
"""
|
||||
with self._lock:
|
||||
return self._do_refresh()
|
||||
|
||||
def get_captcha_token(self, action: str) -> str:
|
||||
"""获取指定 action 的 captcha_token。
|
||||
|
||||
POST /v1/shield/captcha/init
|
||||
Body: {
|
||||
"client_id": "...",
|
||||
"action": "...",
|
||||
"device_id": "...",
|
||||
"meta": {"captcha_sign": "..."}
|
||||
}
|
||||
|
||||
captcha_token 会按 action 缓存,避免重复获取。
|
||||
|
||||
Args:
|
||||
action: 操作类型,如 "restore"、"share" 等。
|
||||
|
||||
Returns:
|
||||
captcha_token 字符串,获取失败返回空字符串。
|
||||
"""
|
||||
with self._lock:
|
||||
# 检查缓存
|
||||
if action in self._captcha_tokens:
|
||||
return self._captcha_tokens[action]
|
||||
return self._do_get_captcha(action)
|
||||
|
||||
def get_headers(self) -> Dict[str, str]:
|
||||
"""构建包含所有认证头的请求头字典。
|
||||
|
||||
返回:
|
||||
- Authorization: Bearer <access_token>
|
||||
- x-captcha-token: <captcha_token> (如有)
|
||||
- x-client-id: <client_id>
|
||||
- x-device-id: <device_id>
|
||||
|
||||
Returns:
|
||||
认证请求头字典。
|
||||
"""
|
||||
self._ensure_token_valid()
|
||||
|
||||
headers: Dict[str, str] = {
|
||||
"x-client-id": self.CLIENT_ID,
|
||||
"x-device-id": self.DEVICE_ID,
|
||||
}
|
||||
|
||||
if self._access_token:
|
||||
headers["Authorization"] = f"Bearer {self._access_token}"
|
||||
|
||||
return headers
|
||||
|
||||
def get_headers_with_captcha(self, action: str = "") -> Dict[str, str]:
|
||||
"""获取带 captcha_token 的完整认证头。
|
||||
|
||||
Args:
|
||||
action: captcha 操作类型,空字符串表示不需要 captcha。
|
||||
|
||||
Returns:
|
||||
包含 Authorization + x-captcha-token 的请求头字典。
|
||||
"""
|
||||
headers = self.get_headers()
|
||||
|
||||
if action:
|
||||
captcha = self.get_captcha_token(action)
|
||||
if captcha:
|
||||
headers["x-captcha-token"] = captcha
|
||||
|
||||
return headers
|
||||
|
||||
def get_access_token(self) -> str:
|
||||
"""获取当前有效的 access_token(必要时自动刷新)。"""
|
||||
self._ensure_token_valid()
|
||||
return self._access_token
|
||||
|
||||
@property
|
||||
def refresh_token(self) -> str:
|
||||
"""返回当前 refresh_token。"""
|
||||
return self._refresh_token
|
||||
|
||||
@refresh_token.setter
|
||||
def refresh_token(self, value: str) -> None:
|
||||
"""更新 refresh_token。"""
|
||||
self._refresh_token = value.strip()
|
||||
with self._lock:
|
||||
self._access_token = ""
|
||||
self._expires_at = 0.0
|
||||
self._captcha_tokens.clear()
|
||||
|
||||
# ─── 内部方法 ──────────────────────────────────────────────
|
||||
|
||||
def _ensure_token_valid(self) -> None:
|
||||
"""确保 access_token 有效(过期则自动刷新)。"""
|
||||
if not self._access_token or time.time() >= (self._expires_at - 60):
|
||||
self.refresh_access_token()
|
||||
|
||||
def _do_refresh(self) -> bool:
|
||||
"""实际执行 token 刷新。
|
||||
|
||||
POST https://xluser-ssl.xunlei.com/v1/auth/token
|
||||
"""
|
||||
if not self._refresh_token:
|
||||
logger.error("[XunleiCredential] 没有 refresh_token,无法刷新")
|
||||
return False
|
||||
|
||||
url = f"{XUNLEI_AUTH_API}/v1/auth/token"
|
||||
body: Dict[str, str] = {
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": self._refresh_token,
|
||||
"client_id": self.CLIENT_ID,
|
||||
}
|
||||
|
||||
try:
|
||||
resp = self._session.post(url, json=body, timeout=30)
|
||||
data = resp.json()
|
||||
|
||||
if resp.status_code != 200:
|
||||
logger.error(
|
||||
"[XunleiCredential] 刷新 token 失败: HTTP %d, %s",
|
||||
resp.status_code,
|
||||
data,
|
||||
)
|
||||
return False
|
||||
|
||||
access_token = data.get("access_token", "")
|
||||
if not access_token:
|
||||
logger.error(
|
||||
"[XunleiCredential] 响应中缺少 access_token: %s", data
|
||||
)
|
||||
return False
|
||||
|
||||
expires_in = int(data.get("expires_in", 7200))
|
||||
new_refresh = data.get("refresh_token", self._refresh_token)
|
||||
|
||||
self._access_token = access_token
|
||||
self._expires_at = time.time() + expires_in
|
||||
|
||||
# 更新 refresh_token(服务端可能下发新的)
|
||||
if new_refresh != self._refresh_token:
|
||||
logger.info(
|
||||
"[XunleiCredential] refresh_token 已轮换: "
|
||||
f"{self._refresh_token[:8]}... → {new_refresh[:8]}..."
|
||||
)
|
||||
self._refresh_token = new_refresh
|
||||
|
||||
# 清除 captcha 缓存(token 变了,captcha 可能也失效了)
|
||||
self._captcha_tokens.clear()
|
||||
|
||||
logger.info(
|
||||
"[XunleiCredential] Token 刷新成功 (expires_in=%ds)", expires_in
|
||||
)
|
||||
return True
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"[XunleiCredential] 刷新 token 网络异常: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.exception(f"[XunleiCredential] 刷新 token 未知异常: {e}")
|
||||
return False
|
||||
|
||||
def _do_get_captcha(self, action: str) -> str:
|
||||
"""获取 captcha_token。
|
||||
|
||||
POST /v1/shield/captcha/init
|
||||
"""
|
||||
url = f"{XUNLEI_AUTH_API}/v1/shield/captcha/init"
|
||||
body: Dict[str, Any] = {
|
||||
"client_id": self.CLIENT_ID,
|
||||
"action": action,
|
||||
"device_id": self.DEVICE_ID,
|
||||
"meta": {
|
||||
"captcha_sign": "",
|
||||
},
|
||||
}
|
||||
|
||||
# 需要 Authorization 头
|
||||
if not self._access_token:
|
||||
if not self._do_refresh():
|
||||
logger.error("[XunleiCredential] 无法获取 access_token,跳过 captcha")
|
||||
return ""
|
||||
|
||||
headers: Dict[str, str] = {
|
||||
"Authorization": f"Bearer {self._access_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
resp = self._session.post(url, json=body, headers=headers, timeout=15)
|
||||
data = resp.json()
|
||||
|
||||
captcha_token = data.get("captcha_token", "")
|
||||
if captcha_token:
|
||||
self._captcha_tokens[action] = captcha_token
|
||||
logger.info(
|
||||
"[XunleiCredential] captcha_token 获取成功 for action=%s",
|
||||
action,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"[XunleiCredential] captcha_token 为空 for action=%s: %s",
|
||||
action,
|
||||
data,
|
||||
)
|
||||
|
||||
return captcha_token
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"[XunleiCredential] 获取 captcha_token 网络异常: {e}")
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.exception(f"[XunleiCredential] 获取 captcha_token 异常: {e}")
|
||||
return ""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"XunleiCredentialManager("
|
||||
f"refresh_token={'***' if self._refresh_token else 'None'}, "
|
||||
f"has_access_token={bool(self._access_token)}, "
|
||||
f"captcha_actions={list(self._captcha_tokens.keys())})"
|
||||
)
|
||||
Reference in New Issue
Block a user