348 lines
10 KiB
Python
348 lines
10 KiB
Python
"""
|
|
CookieCloud客户端核心实现
|
|
"""
|
|
import json
|
|
import time
|
|
import urllib.request
|
|
import urllib.error
|
|
from typing import Dict, List, Optional, Tuple
|
|
from .models import CookieConfig, CookieData, CookieCollection, DownloadResult
|
|
from .exceptions import (
|
|
CookieCloudError,
|
|
ConfigurationError,
|
|
ConnectionError,
|
|
AuthenticationError,
|
|
DataParseError,
|
|
NetworkError
|
|
)
|
|
|
|
|
|
class CookieCloudClient:
|
|
"""
|
|
CookieCloud客户端
|
|
|
|
用于从CookieCloud服务器下载和管理cookies的独立客户端
|
|
|
|
示例:
|
|
>>> config = CookieConfig(
|
|
... server="https://cookiecloud.example.com",
|
|
... username="your_username",
|
|
... password="your_password"
|
|
... )
|
|
>>> client = CookieCloudClient(config)
|
|
>>> result = client.download()
|
|
>>> if result.success:
|
|
... print(f"下载成功,共{result.total_domains}个域名")
|
|
... cookie_str = result.get_cookie_string("example.com")
|
|
"""
|
|
|
|
def __init__(self, config: CookieConfig):
|
|
"""
|
|
初始化客户端
|
|
|
|
Args:
|
|
config: CookieCloud配置对象
|
|
|
|
Raises:
|
|
ConfigurationError: 配置验证失败
|
|
"""
|
|
if not isinstance(config, CookieConfig):
|
|
raise ConfigurationError("配置参数必须是CookieConfig类型")
|
|
|
|
self.config = config
|
|
self._last_download_time = None
|
|
self._download_count = 0
|
|
|
|
def download(self) -> DownloadResult:
|
|
"""
|
|
从CookieCloud服务器下载所有cookies
|
|
|
|
Returns:
|
|
DownloadResult: 下载结果对象
|
|
|
|
Raises:
|
|
ConnectionError: 连接服务器失败
|
|
AuthenticationError: 认证失败
|
|
DataParseError: 数据解析失败
|
|
NetworkError: 网络错误
|
|
"""
|
|
start_time = time.time()
|
|
|
|
try:
|
|
raw_data = self._fetch_data()
|
|
cookies = self._parse_cookies(raw_data)
|
|
|
|
download_time = time.time() - start_time
|
|
self._last_download_time = time.time()
|
|
self._download_count += 1
|
|
|
|
total_cookies = sum(len(c.cookies) for c in cookies.values())
|
|
|
|
return DownloadResult(
|
|
success=True,
|
|
cookies=cookies,
|
|
total_domains=len(cookies),
|
|
total_cookies=total_cookies,
|
|
download_time=download_time
|
|
)
|
|
|
|
except CookieCloudError:
|
|
raise
|
|
except Exception as e:
|
|
raise CookieCloudError(f"下载cookies失败: {str(e)}")
|
|
|
|
def download_for_domain(self, domain: str) -> Optional[str]:
|
|
"""
|
|
下载指定域名的cookie字符串
|
|
|
|
Args:
|
|
domain: 目标域名
|
|
|
|
Returns:
|
|
Optional[str]: cookie字符串,如果不存在则返回None
|
|
|
|
Raises:
|
|
ConnectionError: 连接服务器失败
|
|
AuthenticationError: 认证失败
|
|
DataParseError: 数据解析失败
|
|
"""
|
|
result = self.download()
|
|
return result.get_cookie_string(domain)
|
|
|
|
def download_for_domains(self, domains: List[str]) -> Dict[str, Optional[str]]:
|
|
"""
|
|
批量下载多个域名的cookie字符串
|
|
|
|
Args:
|
|
domains: 目标域名列表
|
|
|
|
Returns:
|
|
Dict[str, Optional[str]]: {domain: cookie_string}
|
|
"""
|
|
result = self.download()
|
|
return {domain: result.get_cookie_string(domain) for domain in domains}
|
|
|
|
def test_connection(self) -> Tuple[bool, str]:
|
|
"""
|
|
测试与CookieCloud服务器的连接
|
|
|
|
Returns:
|
|
Tuple[bool, str]: (是否成功, 消息)
|
|
"""
|
|
try:
|
|
self._fetch_data()
|
|
return True, "连接成功"
|
|
except AuthenticationError as e:
|
|
return False, f"认证失败: {e.message}"
|
|
except ConnectionError as e:
|
|
return False, f"连接失败: {e.message}"
|
|
except Exception as e:
|
|
return False, f"测试失败: {str(e)}"
|
|
|
|
def _fetch_data(self) -> Dict:
|
|
"""
|
|
从服务器获取原始数据
|
|
|
|
Returns:
|
|
Dict: 原始JSON数据
|
|
|
|
Raises:
|
|
ConnectionError: 连接失败
|
|
AuthenticationError: 认证失败
|
|
NetworkError: 网络错误
|
|
"""
|
|
url = f"{self.config.server}/get/{self.config.username}"
|
|
|
|
try:
|
|
data = json.dumps({"password": self.config.password}).encode('utf-8')
|
|
|
|
request = urllib.request.Request(
|
|
url,
|
|
data=data,
|
|
headers={
|
|
'Content-Type': 'application/json',
|
|
'User-Agent': 'CookieCloudClient/1.0'
|
|
},
|
|
method='POST'
|
|
)
|
|
|
|
response = urllib.request.urlopen(
|
|
request,
|
|
timeout=self.config.timeout
|
|
)
|
|
|
|
if response.status != 200:
|
|
if response.status == 401:
|
|
raise AuthenticationError(
|
|
"认证失败,请检查用户名和密码",
|
|
username=self.config.username
|
|
)
|
|
elif response.status == 404:
|
|
raise ConnectionError(
|
|
"用户不存在,请检查用户名",
|
|
server=self.config.server,
|
|
status_code=response.status
|
|
)
|
|
else:
|
|
raise ConnectionError(
|
|
f"服务器返回错误状态码: {response.status}",
|
|
server=self.config.server,
|
|
status_code=response.status
|
|
)
|
|
|
|
result = json.loads(response.read().decode('utf-8'))
|
|
|
|
if not result:
|
|
raise DataParseError("服务器返回数据为空")
|
|
|
|
return result
|
|
|
|
except urllib.error.HTTPError as e:
|
|
if e.code == 401:
|
|
raise AuthenticationError(
|
|
"认证失败,请检查用户名和密码",
|
|
username=self.config.username
|
|
)
|
|
else:
|
|
raise ConnectionError(
|
|
f"HTTP错误: {e.code} {e.reason}",
|
|
server=self.config.server,
|
|
status_code=e.code
|
|
)
|
|
except urllib.error.URLError as e:
|
|
raise NetworkError(
|
|
f"网络连接失败: {e.reason}",
|
|
original_error=e
|
|
)
|
|
except json.JSONDecodeError as e:
|
|
raise DataParseError(
|
|
f"JSON解析失败: {str(e)}"
|
|
)
|
|
except Exception as e:
|
|
if isinstance(e, CookieCloudError):
|
|
raise
|
|
raise NetworkError(
|
|
f"请求失败: {str(e)}",
|
|
original_error=e
|
|
)
|
|
|
|
def _parse_cookies(self, raw_data: Dict) -> Dict[str, CookieCollection]:
|
|
"""
|
|
解析原始cookie数据
|
|
|
|
Args:
|
|
raw_data: 原始JSON数据
|
|
|
|
Returns:
|
|
Dict[str, CookieCollection]: {domain: CookieCollection}
|
|
"""
|
|
if raw_data.get("cookie_data"):
|
|
contents = raw_data.get("cookie_data")
|
|
else:
|
|
contents = raw_data
|
|
|
|
domain_groups = self._group_by_domain(contents)
|
|
|
|
cookies = {}
|
|
for domain, cookie_list in domain_groups.items():
|
|
if not cookie_list:
|
|
continue
|
|
|
|
if self._is_cloudflare_only(cookie_list):
|
|
continue
|
|
|
|
collection = CookieCollection(domain=domain)
|
|
|
|
for cookie_data in cookie_list:
|
|
cookie = CookieData(
|
|
domain=cookie_data.get('domain', ''),
|
|
name=cookie_data.get('name', ''),
|
|
value=cookie_data.get('value', ''),
|
|
path=cookie_data.get('path', '/'),
|
|
secure=cookie_data.get('secure', False),
|
|
http_only=cookie_data.get('httpOnly', False)
|
|
)
|
|
collection.add_cookie(cookie)
|
|
|
|
cookies[domain] = collection
|
|
|
|
return cookies
|
|
|
|
def _group_by_domain(self, contents: Dict) -> Dict[str, List[Dict]]:
|
|
"""
|
|
按域名分组cookies
|
|
|
|
Args:
|
|
contents: 原始cookie内容
|
|
|
|
Returns:
|
|
Dict[str, List[Dict]]: {domain: [cookie_data]}
|
|
"""
|
|
domain_groups = {}
|
|
|
|
for site, cookies in contents.items():
|
|
for cookie in cookies:
|
|
domain = cookie.get("domain", "")
|
|
if not domain:
|
|
continue
|
|
|
|
domain_key = self._extract_domain(domain)
|
|
if not domain_key:
|
|
continue
|
|
|
|
if domain_key not in domain_groups:
|
|
domain_groups[domain_key] = []
|
|
|
|
domain_groups[domain_key].append(cookie)
|
|
|
|
return domain_groups
|
|
|
|
def _extract_domain(self, domain: str) -> Optional[str]:
|
|
"""
|
|
提取主域名
|
|
|
|
Args:
|
|
domain: 原始域名
|
|
|
|
Returns:
|
|
Optional[str]: 主域名
|
|
"""
|
|
if not domain:
|
|
return None
|
|
|
|
domain = domain.lstrip('.')
|
|
|
|
parts = domain.split('.')
|
|
if len(parts) < 2:
|
|
return domain
|
|
|
|
if len(parts) == 2:
|
|
return domain
|
|
|
|
return '.'.join(parts[-2:])
|
|
|
|
def _is_cloudflare_only(self, cookie_list: List[Dict]) -> bool:
|
|
"""
|
|
检查是否仅包含Cloudflare验证cookie
|
|
|
|
Args:
|
|
cookie_list: cookie列表
|
|
|
|
Returns:
|
|
bool: 是否仅包含cf_clearance
|
|
"""
|
|
for cookie in cookie_list:
|
|
if cookie.get("name") != "cf_clearance":
|
|
return False
|
|
return True
|
|
|
|
@property
|
|
def last_download_time(self) -> Optional[float]:
|
|
"""获取最后下载时间"""
|
|
return self._last_download_time
|
|
|
|
@property
|
|
def download_count(self) -> int:
|
|
"""获取下载次数"""
|
|
return self._download_count
|