add-files
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user