"""Cookie Cloud 模块 - 从 Cookie Cloud 服务器获取并处理 cookie 数据""" import json import hashlib import base64 from typing import Dict, List, Optional, Tuple import urllib.request import urllib.error from Crypto.Cipher import AES from dataclasses import dataclass from logger import get_logger logger = get_logger(__name__) @dataclass class CookieCloudConfig: """CookieCloud 配置""" api_url: str uuid: str password: str timeout: int = 30 class CookieCloud: """Cookie Cloud 客户端""" def __init__(self, api_url: str, uuid: str, password: str): """ 初始化 Cookie Cloud 客户端 Args: api_url: Cookie Cloud API 地址 uuid: 用户 UUID password: 用户密码 """ self.config = CookieCloudConfig( api_url=api_url.rstrip('/'), uuid=uuid, password=password, timeout=30 ) def _get_crypt_key(self) -> bytes: """生成加密密钥""" combined_string = f"{self.config.uuid}-{self.config.password}" return hashlib.md5(combined_string.encode('utf-8')).hexdigest()[:16].encode("utf-8") def _bytes_to_key(self, data: bytes, salt: bytes, output: int = 48) -> bytes: """OpenSSL EVP_BytesToKey 密钥派生算法""" assert len(salt) == 8, len(salt) data += salt key = hashlib.md5(data).digest() final_key = key while len(final_key) < output: key = hashlib.md5(key + data).digest() final_key += key return final_key[:output] def _decrypt(self, encrypted: str, passphrase: bytes) -> bytes: """解密数据""" encrypted_bytes = base64.b64decode(encrypted) assert encrypted_bytes.startswith(b"Salted__"), "Invalid encrypted data format" salt = encrypted_bytes[8:16] key_iv = self._bytes_to_key(passphrase, salt, 32 + 16) key = key_iv[:32] iv = key_iv[32:] aes = AES.new(key, AES.MODE_CBC, iv) decrypted_padded = aes.decrypt(encrypted_bytes[16:]) padding_length = decrypted_padded[-1] if isinstance(padding_length, str): padding_length = ord(padding_length) return decrypted_padded[:-padding_length] def _download_all(self) -> Tuple[Optional[Dict], str]: """下载所有cookie和local storage数据""" try: url = f"{self.config.api_url}/get/{self.config.uuid}" request = urllib.request.Request( url, headers={ 'Content-Type': 'application/json', 'User-Agent': 'CookieCloud-Client/1.0' }, method='GET' ) response = urllib.request.urlopen(request, timeout=self.config.timeout) if response.status != 200: return None, f"服务器返回错误状态码: {response.status}" result = json.loads(response.read().decode('utf-8')) if not result: return None, "服务器返回数据为空" encrypted = result.get("encrypted") if not encrypted: return None, "未获取到cookie密文" crypt_key = self._get_crypt_key() try: decrypted_data = self._decrypt(encrypted, crypt_key) result = json.loads(decrypted_data.decode("utf-8")) except Exception as e: return None, f"cookie解密失败: {str(e)}" if not result: return None, "cookie解密为空" return result, "" except urllib.error.HTTPError as e: if e.code == 401: return None, "认证失败,请检查用户名和密码" else: return None, f"HTTP错误: {e.code} {e.reason}" except urllib.error.URLError as e: return None, f"网络连接失败: {e.reason}" except Exception as e: return None, f"下载失败: {str(e)}" def get_cookies(self) -> Dict[str, List[Dict]]: """ 从 Cookie Cloud 服务器获取 cookie 数据 Returns: 按域名分组的 cookie 数据字典 格式: {"domain.com": [{"name": "cookie1", "value": "val1", ...}, ...], ...} """ data, error = self._download_all() if error: logger.error(error) return {} # 处理数据结构 cookie_data = {} # 兼容直接按域名分组的格式 if isinstance(data, dict) and not data.get('cookie_data'): cookie_data = data # 兼容包含 cookie_data 的格式 elif isinstance(data, dict) and data.get('cookie_data'): cookie_data = data.get('cookie_data', {}) # 处理 sameSite 字段 processed_cookies = {} for domain, cookies in cookie_data.items(): if not cookies: continue processed_cookies[domain] = [] for cookie in cookies: if cookie.get('sameSite') == 'unspecified': cookie['sameSite'] = 'Lax' processed_cookies[domain].append(cookie) logger.info(f"获取到 {len(processed_cookies)} 个域名的 cookies") return processed_cookies def get_local_storage(self) -> Dict[str, Dict]: """ 从 Cookie Cloud 服务器获取 local storage 数据 Returns: 按域名分组的 local storage 数据字典 格式: {"domain.com": {"key1": "value1", ...}, ...} """ data, error = self._download_all() if error: logger.error(error) return {} # 处理数据结构 local_storage_data = {} if isinstance(data, dict) and data.get('local_storage_data'): local_storage_data = data.get('local_storage_data', {}) logger.info(f"获取到 {len(local_storage_data)} 个域名的 local storage") return local_storage_data def get_cookies_for_domain(self, domain: str) -> List[Dict]: """ 获取指定域名的 cookie Args: domain: 域名 Returns: cookie 列表 """ all_cookies = self.get_cookies() matched_cookies = [] for cookie_domain, cookies in all_cookies.items(): clean_domain = cookie_domain.lstrip('.') if domain in clean_domain or clean_domain in domain: matched_cookies.extend(cookies) logger.info(f"获取到 {len(matched_cookies)} 个 {domain} 的 cookies") return matched_cookies def get_local_storage_for_domain(self, domain: str) -> Dict: """ 获取指定域名的 local storage Args: domain: 域名 Returns: local storage 字典 """ all_local_storage = self.get_local_storage() for ls_domain, data in all_local_storage.items(): clean_domain = ls_domain.lstrip('.') if domain in clean_domain or clean_domain in domain: logger.info(f"获取到 {domain} 的 local storage") return data return {} def get_all_data(self) -> Dict: """ 获取所有数据(包括 cookies 和 local storage) Returns: 包含所有数据的字典 """ data, error = self._download_all() if error: logger.error(error) return {} result = { 'cookies': {}, 'local_storage': {} } # 处理 cookies if isinstance(data, dict): if not data.get('cookie_data'): result['cookies'] = data elif data.get('cookie_data'): result['cookies'] = data.get('cookie_data', {}) # 处理 local storage if data.get('local_storage_data'): result['local_storage'] = data.get('local_storage_data', {}) logger.info(f"获取到所有数据: {len(result['cookies'])} 个域名的 cookies, {len(result['local_storage'])} 个域名的 local storage") return result