""" 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