Compare commits

...
4 Commits
Author SHA1 Message Date
bwadmin 918ac92230 优化cookie提取器,移除Playwright依赖,重新设计界面和功能 2026-03-04 17:36:09 +08:00
bwadmin c3d07f7325 完善 cookie 监控系统:
1. 修复 browser_login.py 中的 cookie 处理错误
2. 重构 cookie_cloud.py 实现浏览器实例级 cookie 共享
3. 创建 cookie_extractor.py GUI 应用程序,支持打开、保存和恢复功能
4. 优化 cookie 导入方式,支持批量导入和回退机制
5. 修复 localStorage 注入问题,确保特殊字符正确处理
2026-03-03 17:10:50 +08:00
bwadmin 894acc3191 add-files 2026-03-03 08:58:16 +08:00
bwadmin 0fbbbce2ee update 2026-03-03 08:53:50 +08:00
25 changed files with 2770 additions and 698 deletions
+43
View File
@@ -0,0 +1,43 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
env/
ENV/
.venv
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Logs
*.log
log/
# Data files (optional - uncomment if you don't want to commit these)
# cookie_data.json
# cookies.json
# config.json
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+302 -74
View File
@@ -71,29 +71,34 @@ class BrowserLogin:
parsed = urlparse(url)
return parsed.netloc
def _get_cookies_dict(self) -> Dict[str, List[Dict]]:
def get_all_cookies(self) -> List[Dict]:
"""
获取当前浏览器的所有 cookie,按域名分组
获取当前浏览器实例的所有 cookie
Returns:
按域名分组的 cookie 字典
所有 cookie 的列表
"""
cookies = self.tab.cookies.as_dict
# 先获取 cookies 对象,然后调用 as_dict 方法
cookies_obj = self.tab.cookies
cookies = cookies_obj() if callable(cookies_obj) else cookies_obj
cookies = cookies.as_dict() if hasattr(cookies, 'as_dict') else cookies
# 按域名分组
cookie_dict = {}
# 确保返回完整的 cookie 列表,包含所有属性
formatted_cookies = []
for cookie in cookies:
domain = cookie.get('domain', '')
# 统一域名格式(去掉开头的点)
if domain.startswith('.'):
domain = domain[1:]
formatted_cookie = {
'name': cookie.get('name', ''),
'value': cookie.get('value', ''),
'domain': cookie.get('domain', ''),
'path': cookie.get('path', '/'),
'expires': cookie.get('expirationDate', None),
'secure': cookie.get('secure', False),
'httpOnly': cookie.get('httpOnly', False),
'sameSite': cookie.get('sameSite', 'Lax')
}
formatted_cookies.append(formatted_cookie)
if domain not in cookie_dict:
cookie_dict[domain] = []
cookie_dict[domain].append(cookie)
return cookie_dict
return formatted_cookies
def check_login(self, url: str, check_selector: str, success_text: str) -> bool:
"""
@@ -129,6 +134,114 @@ class BrowserLogin:
logger.error(f"检查登录状态失败: {e}")
return False
def import_all_cookies(self, cookies: List[Dict]) -> bool:
"""
一次性导入所有 cookies 到浏览器
Args:
cookies: cookie 列表
Returns:
是否成功
"""
if not self.browser or not self.tab:
self._create_browser()
success_count = 0
fail_count = 0
logger.info(f"开始导入 {len(cookies)} 个 cookie")
try:
# 准备所有 cookie 数据
formatted_cookies = []
for cookie in cookies:
cookie_dict = {
'name': cookie.get('name', ''),
'value': cookie.get('value', ''),
'domain': cookie.get('domain', ''),
'path': cookie.get('path', '/'),
}
if cookie.get('secure'):
cookie_dict['secure'] = True
if cookie.get('httpOnly'):
cookie_dict['httpOnly'] = True
if cookie.get('sameSite'):
cookie_dict['sameSite'] = cookie.get('sameSite')
formatted_cookies.append(cookie_dict)
# 根据 DrissionPage 文档,set.cookies() 方法可以直接接收列表格式的多个 cookie
logger.info("使用一次性注入方式导入 cookie")
self.tab.set.cookies(formatted_cookies)
success_count = len(formatted_cookies)
except Exception as e:
logger.error(f"导入 cookie 失败: {e}")
# 回退到逐个注入
logger.info("回退到逐个注入方式导入 cookie")
for cookie_dict in formatted_cookies:
try:
self.tab.set.cookies(cookie_dict)
success_count += 1
except Exception as e:
fail_count += 1
logger.info(f"Cookie 导入完成: 成功 {success_count}, 失败 {fail_count}")
return fail_count == 0
def verify_login(self, url: str, check_selector: str = "", success_text: str = "") -> bool:
"""
验证登录状态(cookies已预先导入)
Args:
url: 网站地址
check_selector: 登录检测选择器(可选)
success_text: 登录成功时显示的文本(可选)
Returns:
是否已登录
"""
if not self.browser or not self.tab:
self._create_browser()
try:
self.tab.get(url)
time.sleep(5)
# 如果没有指定选择器,只检查页面是否成功加载
if not check_selector:
logger.info(" 未指定登录检测选择器,跳过登录验证")
return True
# 尝试查找元素
try:
element = self.tab.ele(check_selector, timeout=10)
if element:
element_text = element.text or ""
logger.info(f" 找到元素,文本内容: {element_text[:50]}...")
if not success_text:
logger.info(" 未指定成功文本,找到元素即认为登录成功")
return True
if success_text in element_text:
logger.info(f" 检测到成功文本: {success_text}")
return True
else:
logger.warning(f" 未检测到成功文本 '{success_text}',元素文本: {element_text[:100]}")
return False
else:
logger.warning(f" 未找到元素: {check_selector}")
return False
except Exception as ele_error:
logger.warning(f" 查找元素失败: {ele_error}")
return False
except Exception as e:
logger.error(f"验证登录状态失败: {e}")
return False
def login_with_cookies(
self,
url: str,
@@ -152,20 +265,38 @@ class BrowserLogin:
self._create_browser()
try:
# 访问网站
from urllib.parse import urlparse
parsed = urlparse(url)
base_url = f"{parsed.scheme}://{parsed.netloc}"
self.tab.get(base_url)
time.sleep(1)
logger.info(f"开始设置 {len(cookies)} 个 cookie")
for cookie in cookies:
try:
cookie_dict = {
'name': cookie.get('name', ''),
'value': cookie.get('value', ''),
'domain': cookie.get('domain', parsed.netloc),
'path': cookie.get('path', '/'),
}
if cookie.get('secure'):
cookie_dict['secure'] = True
if cookie.get('httpOnly'):
cookie_dict['httpOnly'] = True
if cookie.get('sameSite'):
cookie_dict['sameSite'] = cookie.get('sameSite')
self.tab.set.cookies(cookie_dict)
logger.info(f"设置 cookie: {cookie_dict['name']}")
except Exception as e:
logger.error(f"设置 cookie 失败 {cookie.get('name')}: {e}")
self.tab.get(url)
# 添加 cookies
for cookie in cookies:
self.tab.set.cookies(cookie)
# 刷新页面
self.tab.refresh()
# 等待页面加载
time.sleep(3)
# 检查登录状态
element = self.tab.ele(check_selector, timeout=10)
if element and element.text and success_text in element.text:
@@ -198,9 +329,19 @@ class BrowserLogin:
time.sleep(3)
# 获取 cookies
cookies = self.tab.cookies.as_dict
cookies_obj = self.browser.cookies
cookies = cookies_obj() if callable(cookies_obj) else cookies_obj
cookies = cookies.as_dict() if hasattr(cookies, 'as_dict') else cookies
return cookies
# 确保返回的是列表
if isinstance(cookies, list):
return cookies
elif isinstance(cookies, dict):
# 如果是字典,转换为列表
return [cookies]
else:
# 如果是其他类型,返回空列表
return []
except Exception as e:
logger.error(f"刷新页面并保存 cookie 失败: {e}")
@@ -212,7 +353,7 @@ class BrowserLogin:
class CookieManager:
"""Cookie 管理器"""
"""Cookie 管理器 - 实现浏览器实例级别的 Cookie 共享"""
def __init__(self, cookie_file: str):
"""
@@ -224,14 +365,25 @@ class CookieManager:
self.cookie_file = cookie_file
self.cookies = self._load_cookies()
def _load_cookies(self) -> Dict[str, Dict[str, List[Dict]]]:
def _load_cookies(self) -> Dict[str, List[Dict]]:
"""加载 cookie 文件"""
import os
if os.path.exists(self.cookie_file):
try:
with open(self.cookie_file, 'r', encoding='utf-8') as f:
return json.load(f)
data = json.load(f)
# 兼容旧格式
if isinstance(data, dict) and any(isinstance(v, dict) for v in data.values()):
# 转换旧格式为新格式
new_data = {}
for user, websites in data.items():
all_cookies = []
for site_cookies in websites.values():
all_cookies.extend(site_cookies)
new_data[user] = all_cookies
return new_data
return data
except Exception as e:
logger.error(f"加载 cookie 文件失败: {e}")
@@ -245,76 +397,152 @@ class CookieManager:
except Exception as e:
logger.error(f"保存 cookie 文件失败: {e}")
def get_cookies(self, user_name: str, website_name: str) -> List[Dict]:
def get_all_cookies(self, user_name: str) -> List[Dict]:
"""
获取指定用户和网站的 cookie
获取指定用户的所有 cookie
Args:
user_name: 用户名
website_name: 网站名
Returns:
cookie 列表
"""
if user_name not in self.cookies:
return []
return self.cookies.get(user_name, [])
if website_name not in self.cookies[user_name]:
return []
return self.cookies[user_name][website_name]
def save_cookies(self, user_name: str, website_name: str, cookies: List[Dict]):
def save_all_cookies(self, user_name: str, cookies: List[Dict]):
"""
保存指定用户和网站的 cookie
保存指定用户的所有 cookie
Args:
user_name: 用户名
website_name: 网站名
cookies: cookie 列表
"""
if user_name not in self.cookies:
self.cookies[user_name] = {}
self.cookies[user_name][website_name] = cookies
self.cookies[user_name] = cookies
self._save_cookies()
def get_cookies_for_domain(self, domain: str) -> List[Dict]:
def save_cookies(self, user_name: str, website_name: str, cookies: List[Dict]):
"""
获取指定域名的 cookie(从所有用户的 cookie 中查找
保存指定用户和网站的 cookie(兼容旧接口
Args:
user_name: 用户名
website_name: 网站名(未使用,保持兼容)
cookies: cookie 列表
"""
# 保持向后兼容,将网站的cookie添加到用户的所有cookie中
if user_name not in self.cookies:
self.cookies[user_name] = []
# 合并新cookie到现有cookie中
existing_cookies = self.cookies[user_name]
# 创建现有cookie的索引,用于快速查找
cookie_index = {}
for i, cookie in enumerate(existing_cookies):
if isinstance(cookie, dict):
key = (cookie.get('name'), cookie.get('domain'), cookie.get('path'))
cookie_index[key] = i
# 添加或更新cookie
if isinstance(cookies, list):
valid_cookies = []
for cookie in cookies:
if isinstance(cookie, dict):
valid_cookies.append(cookie)
for cookie in valid_cookies:
key = (cookie.get('name'), cookie.get('domain'), cookie.get('path'))
if key in cookie_index:
# 更新现有cookie
existing_cookies[cookie_index[key]] = cookie
else:
# 添加新cookie
existing_cookies.append(cookie)
if valid_cookies:
logger.info(f" 已保存 {len(valid_cookies)} 个新 cookie")
self._save_cookies()
def add_cookie(self, user_name: str, cookie: Dict):
"""
为指定用户添加单个 cookie
Args:
user_name: 用户名
cookie: cookie 字典
"""
if user_name not in self.cookies:
self.cookies[user_name] = []
# 检查是否已存在相同的 cookie
existing_index = -1
for i, existing_cookie in enumerate(self.cookies[user_name]):
if (existing_cookie.get('name') == cookie.get('name') and
existing_cookie.get('domain') == cookie.get('domain') and
existing_cookie.get('path') == cookie.get('path')):
existing_index = i
break
if existing_index >= 0:
# 更新现有 cookie
self.cookies[user_name][existing_index] = cookie
else:
# 添加新 cookie
self.cookies[user_name].append(cookie)
self._save_cookies()
def remove_cookie(self, user_name: str, cookie_name: str, domain: str):
"""
从指定用户中移除指定的 cookie
Args:
user_name: 用户名
cookie_name: cookie 名称
domain: cookie 域名
"""
if user_name in self.cookies:
self.cookies[user_name] = [
cookie for cookie in self.cookies[user_name]
if not (cookie.get('name') == cookie_name and cookie.get('domain') == domain)
]
self._save_cookies()
def clear_cookies(self, user_name: str):
"""
清空指定用户的所有 cookie
Args:
user_name: 用户名
"""
if user_name in self.cookies:
self.cookies[user_name] = []
self._save_cookies()
def get_cookies_for_domain(self, user_name: str, domain: str) -> List[Dict]:
"""
获取指定用户的指定域名的 cookie
Args:
user_name: 用户名
domain: 域名
Returns:
cookie 列表
"""
all_cookies = []
user_cookies = self.get_all_cookies(user_name)
filtered_cookies = []
for user_name, websites in self.cookies.items():
for website_name, cookies in websites.items():
for cookie in cookies:
cookie_domain = cookie.get('domain', '')
# 统一域名格式
if cookie_domain.startswith('.'):
cookie_domain = cookie_domain[1:]
for cookie in user_cookies:
cookie_domain = cookie.get('domain', '')
# 统一域名格式
if cookie_domain.startswith('.'):
cookie_domain = cookie_domain[1:]
if domain in cookie_domain or cookie_domain in domain:
all_cookies.append(cookie)
if domain in cookie_domain or cookie_domain in domain:
filtered_cookies.append(cookie)
return all_cookies
return filtered_cookies
if __name__ == "__main__":
# 测试代码
manager = CookieManager("cookies.json")
# 保存测试 cookie
test_cookies = [
{"name": "test", "value": "123", "domain": "example.com", "path": "/"}
]
manager.save_cookies("测试用户", "测试网站", test_cookies)
# 获取 cookie
cookies = manager.get_cookies("测试用户", "测试网站")
logger.info(f"获取到的 cookie: {cookies}")
+179
View File
@@ -0,0 +1,179 @@
import tkinter as tk
from tkinter import ttk, messagebox
from DrissionPage import ChromiumPage, ChromiumOptions
import json
import threading
import time
class BrowserTool:
def __init__(self, root):
self.root = root
self.root.title('浏览器Cookie管理工具')
self.root.geometry('500x200')
self.root.resizable(False, False)
self.page = None
self.cookie_file = 'd:\\test\\openUrl\\cookies.json'
self.setup_ui()
def setup_ui(self):
frame = ttk.Frame(self.root, padding=20)
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text='网址:').grid(row=0, column=0, sticky=tk.W, pady=5)
self.url_entry = ttk.Entry(frame, width=60)
self.url_entry.grid(row=0, column=1, columnspan=3, pady=5, sticky=tk.W)
self.url_entry.insert(0, 'https://yunlu.sinopec.com/AddressBook/AddressBookCheck')
btn_frame = ttk.Frame(frame)
btn_frame.grid(row=1, column=0, columnspan=4, pady=20)
self.open_btn = ttk.Button(btn_frame, text='打开', width=15, command=self.open_browser)
self.open_btn.pack(side=tk.LEFT, padx=10)
self.save_btn = ttk.Button(btn_frame, text='保存', width=15, command=self.save_cookies)
self.save_btn.pack(side=tk.LEFT, padx=10)
self.import_btn = ttk.Button(btn_frame, text='导入', width=15, command=self.import_cookies)
self.import_btn.pack(side=tk.LEFT, padx=10)
self.status_var = tk.StringVar(value='就绪')
ttk.Label(frame, textvariable=self.status_var).grid(row=2, column=0, columnspan=4, pady=10)
def log(self, msg):
self.status_var.set(msg)
self.root.update()
def init_browser(self):
co = ChromiumOptions()
co.set_browser_path(r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe')
self.page = ChromiumPage(addr_or_opts=co)
def wait_page_loaded(self, timeout=30):
if self.page:
self.page.wait.doc_loaded(timeout=timeout)
self.page.ele('tag:body', timeout=timeout)
def open_browser(self):
def run():
try:
self.log('正在启动浏览器...')
self.init_browser()
url = self.url_entry.get().strip()
if not url:
messagebox.showerror('错误', '请输入网址')
return
self.log(f'正在打开: {url}')
self.page.get(url)
self.wait_page_loaded()
self.log('页面加载完成')
except Exception as e:
self.log(f'错误: {str(e)}')
messagebox.showerror('错误', str(e))
threading.Thread(target=run, daemon=True).start()
def save_cookies(self):
def run():
try:
if not self.page:
messagebox.showwarning('提示', '请先打开浏览器')
return
self.log('正在保存Cookies...')
cookies = self.page.cookies()
with open(self.cookie_file, 'w', encoding='utf-8') as f:
json.dump(cookies, f, ensure_ascii=False, indent=2)
self.log(f'Cookies已保存到 {self.cookie_file}')
self.log('正在关闭浏览器...')
self.page.quit()
self.page = None
self.log('浏览器已关闭')
messagebox.showinfo('成功', 'Cookies保存成功,浏览器已关闭')
except Exception as e:
self.log(f'错误: {str(e)}')
messagebox.showerror('错误', str(e))
threading.Thread(target=run, daemon=True).start()
def import_cookies(self):
def run():
try:
import os
if not os.path.exists(self.cookie_file):
messagebox.showwarning('提示', 'Cookie文件不存在,请先保存Cookies')
return
self.log('正在启动浏览器...')
self.init_browser()
self.log('正在加载Cookies...')
with open(self.cookie_file, 'r', encoding='utf-8') as f:
cookies = json.load(f)
url = self.url_entry.get().strip()
if not url:
messagebox.showerror('错误', '请输入网址')
return
from urllib.parse import urlparse
parsed = urlparse(url)
domain = parsed.netloc
self.page.get('about:blank')
for cookie in cookies:
cookie_dict = {
'name': cookie.get('name'),
'value': cookie.get('value'),
'domain': cookie.get('domain', domain),
'path': cookie.get('path', '/')
}
try:
self.page.set.cookies(cookie_dict)
except Exception as ce:
self.log(f'设置Cookie失败: {cookie.get("name")} - {str(ce)}')
self.log(f'已导入 {len(cookies)} 个Cookie')
self.log(f'正在打开: {url}')
self.page.get(url)
self.wait_page_loaded()
self.log('刷新页面验证登录状态...')
for i in range(3):
self.page.refresh()
self.wait_page_loaded()
time.sleep(1)
self.log(f'刷新第 {i+1} 次...')
self.log('页面加载完成')
except Exception as e:
self.log(f'错误: {str(e)}')
messagebox.showerror('错误', str(e))
threading.Thread(target=run, daemon=True).start()
def on_closing(self):
if self.page:
try:
self.page.quit()
except:
pass
self.root.destroy()
if __name__ == '__main__':
root = tk.Tk()
app = BrowserTool(root)
root.protocol('WM_DELETE_WINDOW', app.on_closing)
root.mainloop()
+52 -1
View File
@@ -1 +1,52 @@
{"users": [{"name": "用户A", "cookie_cloud": {"uuid": "hu6n2vcUmzpu7mqUN2rVCg", "password": "jtDM5dV9AyqXkZdQVeA9f6", "api_url": "https://movie-pilot.org/cookiecloud"}, "notification": {"iyuu_token": "IYUU37629Tc1d371c7ce99a49ff9778e196286b7e4592be191", "max_fail_count": 3}, "browser": {"type": "edge", "headless": false}, "websites": [{"name": "网站A", "url": "https://lmkbi.95155.com/bi-system/#/carUnpetrifiedComAll", "login_check_selector": "#user-info", "success_text": "彭峰"}]}]}
{
"users": [
{
"name": "用户A",
"cookie_cloud": {
"uuid": "hu6n2vcUmzpu7mqUN2rVCg",
"password": "jtDM5dV9AyqXkZdQVeA9f6",
"api_url": "https://movie-pilot.org/cookiecloud"
},
"notification": {
"iyuu_token": "IYUU37629Tc1d371c7ce99a49ff9778e196286b7e4592be191",
"max_fail_count": 3
},
"browser": {
"type": "edge",
"headless": false
},
"websites": [
{
"name": "网站A",
"url": "https://lmkbi.95155.com/bi-system/#/carUnpetrifiedComAll",
"login_check_selector": "",
"success_text": "彭峰"
}
]
},
{
"name": "用户B",
"cookie_cloud": {
"uuid": "qyZpTrgiP5mVwiRZwfBhjz",
"password": "iGn1B4FnWftp3oj4Ko3jxA",
"api_url": "http://192.168.1.100:3000/cookiecloud"
},
"notification": {
"iyuu_token": "IYUU37629Tc1d371c7ce99a49ff9778e196286b7e4592be191",
"max_fail_count": 3
},
"browser": {
"type": "edge",
"headless": false
},
"websites": [
{
"name": "网站A",
"url": "https://lmkbi.95155.com/bi-system/#/carUnpetrifiedComAll",
"login_check_selector": "",
"success_text": ""
}
]
}
]
}
+201 -126
View File
@@ -1,16 +1,26 @@
"""Cookie Cloud 模块 - 从 Cookie Cloud 服务器获取并解密 cookie 数据"""
import hashlib
"""Cookie Cloud 模块 - 从 Cookie Cloud 服务器获取并处理 cookie 数据"""
import json
import hashlib
import base64
from typing import Dict, List, Optional
import requests
from typing import Dict, List, Optional, Tuple
import urllib.request
import urllib.error
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from dataclasses import dataclass
from applogger 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 客户端"""
@@ -23,81 +33,92 @@ class CookieCloud:
uuid: 用户 UUID
password: 用户密码
"""
self.api_url = api_url.rstrip('/')
self.uuid = uuid
self.password = password
self.config = CookieCloudConfig(
api_url=api_url.rstrip('/'),
uuid=uuid,
password=password,
timeout=30
)
def decrypt(self, encrypted_data: str) -> Dict:
"""
解密 Cookie Cloud 数据
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")
Args:
encrypted_data: 加密的数据字符串
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]
Returns:
解密后的数据字典,包含 cookie_data 和 local_storage_data
"""
# 生成解密密钥: md5(uuid+password) 取前 16 个字符
key_str = f"{self.uuid}{self.password}"
key = hashlib.md5(key_str.encode()).hexdigest()[:16].encode()
print(f"生成的密钥: {key}")
print(f"密钥长度: {len(key)}")
# 解码 Base64
encrypted = base64.b64decode(encrypted_data)
print(f"Base64 解码后长度: {len(encrypted)}")
print(f"解码后前 20 字节: {encrypted[:20]}")
# 检查是否有盐值(OpenSSL 格式)
if encrypted.startswith(b'Salted__'):
print("检测到 OpenSSL 格式数据")
# 对于 OpenSSL 格式,我们需要跳过盐值部分
# 但根据用户要求,我们直接使用16字符密钥,不考虑盐值
ciphertext = encrypted[16:] # 跳过 "Salted__" 和盐值
print(f"密文长度: {len(ciphertext)}")
else:
print("未检测到 OpenSSL 格式,使用整个数据作为密文")
ciphertext = encrypted
# 使用固定的 IV (16 个 0 字节)
iv = b'\x00' * 16
print(f"使用的 IV: {iv}")
# AES-128-CBC 解密(16字节密钥)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(ciphertext)
print(f"解密后长度: {len(decrypted)}")
print(f"解密后前 100 字节: {decrypted[:100]}")
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:
# 移除 PKCS#7 填充
decrypted = unpad(decrypted, AES.block_size)
print(f"移除填充后长度: {len(decrypted)}")
print(f"移除填充后前 100 字节: {decrypted[:100]}")
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'
)
# 尝试解析 JSON
result = json.loads(decrypted.decode('utf-8'))
print("JSON 解析成功")
return result
except Exception as e:
print(f"解密失败: {e}")
# 尝试直接截取可能的有效数据
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:
# 尝试找到 JSON 开始的位置
for i in range(len(decrypted)):
try:
test_data = decrypted[i:]
test_str = test_data.decode('utf-8', errors='ignore')
if test_str.strip().startswith('{'):
print(f"找到可能的 JSON 开始位置: {i}")
result = json.loads(test_str)
print("JSON 解析成功(直接截取)")
return result
except:
continue
except:
pass
raise
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]]:
"""
@@ -107,50 +128,59 @@ class CookieCloud:
按域名分组的 cookie 数据字典
格式: {"domain.com": [{"name": "cookie1", "value": "val1", ...}, ...], ...}
"""
url = f"{self.api_url}/get/{self.uuid}"
data, error = self._download_all()
try:
response = requests.get(url, timeout=30)
response.raise_for_status()
data = response.json()
logger.info(f"Cookie Cloud 响应: {json.dumps(data, indent=2)}")
if not data or 'encrypted' not in data:
logger.error("响应中没有 encrypted 字段")
return {}
# 打印完整的加密数据长度和前200个字符
logger.info(f"加密数据长度: {len(data['encrypted'])}")
logger.info(f"加密数据前 200 个字符: {data['encrypted'][:200]}...")
try:
decrypted_data = self.decrypt(data['encrypted'])
logger.info(f"解密后的数据: {json.dumps(decrypted_data, indent=2)}")
except Exception as e:
logger.error(f"解密失败: {e}")
return {}
# 解析 cookie_data
cookie_dict = {}
if 'cookie_data' in decrypted_data:
for domain, cookies in decrypted_data['cookie_data'].items():
cookie_dict[domain] = []
for cookie in cookies:
# 处理 sameSite 字段
if cookie.get('sameSite') == 'unspecified':
cookie['sameSite'] = 'Lax'
cookie_dict[domain].append(cookie)
return cookie_dict
except requests.RequestException as e:
logger.error(f"请求 Cookie Cloud 失败: {e}")
if error:
logger.error(error)
return {}
except Exception as e:
logger.error(f"处理 Cookie Cloud 数据失败: {e}")
# 处理数据结构
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
@@ -163,21 +193,66 @@ class CookieCloud:
"""
all_cookies = self.get_cookies()
# 查找匹配的域名
matched_cookies = []
for cookie_domain, cookies in all_cookies.items():
if domain in cookie_domain or cookie_domain in domain:
return cookies
clean_domain = cookie_domain.lstrip('.')
if domain in clean_domain or clean_domain in domain:
matched_cookies.extend(cookies)
return []
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
if __name__ == "__main__":
# 测试代码
cc = CookieCloud(
api_url="https://movie-pilot.org/cookiecloud",
uuid="hu6n2vcUmzpu7mqUN2rVCg",
password="jtDM5dV9AyqXkZdQVeA9f6"
)
cookies = cc.get_cookies()
logger.info(json.dumps(cookies, indent=2, ensure_ascii=False))
+160
View File
@@ -0,0 +1,160 @@
{
"cookies": [
{
"name": "JSESSIONID",
"value": "DD8A9B4F6E06DF35AE1EC2339DF0FA36",
"domain": "10.190.1.205"
},
{
"name": "__rubyUX",
"value": "false",
"domain": "ntp.msn.cn"
},
{
"name": "pglt-edgeChromium-dhp",
"value": "2083",
"domain": ".msn.cn"
},
{
"name": "pglt-edgeChromium-ntp",
"value": "2083",
"domain": ".msn.cn"
},
{
"name": "_C_Auth",
"value": "",
"domain": "ntp.msn.cn"
},
{
"name": "MicrosoftApplicationsTelemetryDeviceId",
"value": "da0bf04e-2115-4952-a012-fc29b4954ec0",
"domain": "ntp.msn.cn"
},
{
"name": "USRLOC",
"value": "",
"domain": ".msn.cn"
},
{
"name": "MUID",
"value": "15CB8BCF863F643B03449CDE8778658F",
"domain": ".msn.cn"
},
{
"name": "v1",
"value": "ExJ_flr=b!>Rb_?o_sb/",
"domain": ".mediav.com"
},
{
"name": "MUID",
"value": "15CB8BCF863F643B03449CDE8778658F",
"domain": ".bing.com"
},
{
"name": "MR",
"value": "0",
"domain": ".c.bing.com"
},
{
"name": "SRM_B",
"value": "15CB8BCF863F643B03449CDE8778658F",
"domain": ".c.bing.com"
},
{
"name": "MR",
"value": "0",
"domain": ".c.msn.cn"
},
{
"name": "MUIDB",
"value": "15CB8BCF863F643B03449CDE8778658F",
"domain": "ntp.msn.cn"
},
{
"name": ".ASPXANONYMOUS",
"value": "21JPwTAtPyn84bHA6aNjbcWhPsVrLyiIZ-4mnJ6_dMtvLzgXbw3J9nyy0dxCZj0b8vOEHXZpgfYoPylWhp5s3AY2GwcwdYg9JI6VqZ6YK3uRkwMf0",
"domain": "i3.sinopec.com"
},
{
"name": "x",
"value": "x",
"domain": "auth.siam.sinopec.com"
},
{
"name": "msnup",
"value": "%7B%22cnex%22%3A%22no%22%7D",
"domain": ".msn.cn"
},
{
"name": "msaoptout",
"value": "0",
"domain": "ntp.msn.cn"
},
{
"name": "OptanonAlertBoxClosed",
"value": "2026-03-04T08:18:01.713Z",
"domain": ".msn.cn"
},
{
"name": "eupubconsent-v2",
"value": "CQghjcAQghjcAAcABBZHBfFsAP_gAELgACiQK4tX_G__bXlr8X73aftkeY1f99h77sQxBhaJk-4FzJvW_JwX32E7NAz6tqYKmRIAu3TBAQNlHJDURVCgaIgVqSDMaEyUoTNKJ6BkiFMRY2dYCFxvm4tjeQCY5vr991d52B-t7dr83dzyy4hHv3a5_0S1WAAAAYCNDfv9bROb-9IO9_x8v4v4_F7pE2_eS1l_tWvp7D9-cts_9XW99_bbff9Pn_-uF_-_X_vf_H37v9oK5AACAAAARgkCAAAgAAAAABAAAAAQAAAAAQBgAAARBAEAAgAQGEgAEAAAAIAAAAAgSAAAAAJAAgAAAABQAAAIBAAAAwAIBgAAGAAACABACAAAAgEgYJgQQCBAABCYBAAgQgAAAACwECAABAgiACEQcAQAAAAAAAAEAAoAAEAgDAQkkBCxIIAuIJoAACAAAIIAChFJ2YAggDNlqrwZNoytMCwfMFzymAZAEQRk5JsQmsAoJA1AAQAAuACgAKgAcAA8ACCAGQAagA8ACIAEwAKoAbwA9AB-AEJAIYAiQBHACWAE0AK0AYYAywBsgDvgHsAfEA-wD9AIBARcBGACNAFBAKgAVcAuYBigDaAG4AOIAkQBOwChwFHgKRAWwAuQBd4DDQGSAMnAZcAzmBrAGsgNvAeOEAMAAOAHOAQcAn4CPQEigJWATaAsIBeQDEAGLQMhAyMBowDUwG0ANuAboA8oB8gD9wICAQMAgiOATgAIgAcAB4AFwAfgBoAHOAO4AgEBBwEIAJ-AVAAvQB0gEegJFASsAmIBMoCbQFIAKTAWoAvoBiADFgGQgMmAaMA00BqYDXgG0ANuAeUA-IB9sD9gP3AgeBBEdBAAAXABQAFQAOAAgABdADIANQAeABEACYAFWALgAugBiADeAHoAP0AhgCJAEsAJoAUYArQBhgDKAGiANkAd4A9oB9gH7ARYBGACggFXALEAXMAvIBigDaAG4AOIAdQBF4CRAEyAJ2AUOAo8BTQCxQFsALgAXIAu0Bd4DDQGPAMkAZOAyqBlgGXAM5AaqA1gBt4DxwH1gQBIAHAAEABoAHOAWIBB4CPQE2gKTAVKAvIBqYDbAG3APKAfEA_YCB4EGAINgQrAimBGkCN8EkQSSAqmBWkCtsFcQVyIQIwAFgAUABcAFUALgAYgA3gB6AEcAO8AigBKQCggFXALmAYoA2gB1IFNAU2AqwBYoC0QFwALkAZOAzkBqoDxwH9gQtAh6BIoCSAFSgKvkoD4ACAAFgAUAA4ADwAIgATAAqgBcADFAIYAiQBHACjAFaANkAd4A_ACrgGKAOoAi8BIgCjwFigLYAZOAywBnIDWAG3gQPJADQALgDuAIAAVABHoCRQErAJtAUmAxYB5QD9wIIlIFwAC4AKAAqABwAEEAMgA0AB4AEQAJgAVQAxAB-gEMARIAowBWgDKAGiANkAd8A-wD9AIsARgAoIBVwC5gF5AMUAbQA3ACLwEiAJ2AUOApoBYoC2AFwALkAXaAw0BkgDJwGXAM5gawBrIDbwHjlADYAFwBHADnAHcAQAAkQBYgDXgHbAP-Aj0BIoCYgE2gKQAU-AvIBfQDFgGTANTAa8A8oB8UD9gP3AgYBA8.f_wACFwAAAAA",
"domain": ".msn.cn"
},
{
"name": "MSFPC",
"value": "GUID=139384d4545d4f4583ff3ef161faee39&HASH=1393&LV=202603&V=4&LU=1772612281994",
"domain": "ntp.msn.cn"
},
{
"name": "UID",
"value": "1862498edfbe5a24e12c8d21772613043",
"domain": ".scorecardresearch.com"
},
{
"name": "XID",
"value": "1862498edfbe5a24e12c8d21772613043",
"domain": ".scorecardresearch.com"
},
{
"name": "SM",
"value": "C",
"domain": ".c.msn.cn"
},
{
"name": "JSESSIONID",
"value": "DD8A9B4F6E06DF35AE1EC2339DF0FA36",
"domain": "10.190.1.205"
},
{
"name": "language",
"value": "zh-CN",
"domain": "192.168.8.147"
},
{
"name": "fnos-token",
"value": "n4fYErf2p2lHSucAt8uYAo+M+JDy354smCjF98dHtJM=",
"domain": "192.168.8.147"
},
{
"name": "fnos-long-token",
"value": "07MRUOgDAAC3g89pAAAAAMrPwB8hdcC8nqwi0bqBWfNDO0uPBM7fPQ==",
"domain": "192.168.8.147"
},
{
"name": "_C_ETH",
"value": "1",
"domain": ".msn.cn"
},
{
"name": "_EDGE_S",
"value": "\"SID=217608637BE1667B39501F717A966764\"",
"domain": ".msn.cn"
},
{
"name": "OptanonConsent",
"value": "isGpcEnabled=0&datestamp=Wed+Mar+04+2026+17%3A09%3A17+GMT%2B0800+(%E4%B8%AD%E5%9B%BD%E6%A0%87%E5%87%86%E6%97%B6%E9%97%B4)&version=202501.2.0&browserGpcFlag=0&isIABGlobal=false&hosts=&consentId=3599d387-f02a-409c-910d-0a6caea03c05&interactionCount=1&isAnonUser=1&landingPath=NotLandingPage&groups=C0001%3A1%2CC0002%3A1%2CC0004%3A1%2CC0008%3A1%2CV2STACK42%3A1&AwaitingReconsent=false&intType=1&geolocation=%3B",
"domain": ".msn.cn"
}
],
"url": "http://192.168.8.147:5666/login"
}
+263
View File
@@ -0,0 +1,263 @@
"""Cookie提取器 - 使用DrissionPage库"""
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import logging
from DrissionPage import Chromium, ChromiumOptions
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('cookie_extractor.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class CookieExtractorApp:
"""Cookie提取器应用"""
def __init__(self, root):
self.root = root
self.root.title("Cookie提取器")
self.root.geometry("600x250")
self.root.resizable(True, True)
self.browser = None
self.tab = None
self._create_widgets()
def _create_widgets(self):
main_frame = ttk.Frame(self.root, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
url_frame = ttk.LabelFrame(main_frame, text="网址输入", padding="10")
url_frame.pack(fill=tk.X, pady=10)
ttk.Label(url_frame, text="网址:").pack(side=tk.LEFT, padx=5)
self.url_var = tk.StringVar(value="http://192.168.8.147:5666/login")
self.url_entry = ttk.Entry(url_frame, textvariable=self.url_var, width=50)
self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=10)
self.open_button = ttk.Button(button_frame, text="打开", command=self.open_website, width=15)
self.open_button.pack(side=tk.LEFT, padx=5)
self.save_button = ttk.Button(button_frame, text="保存", command=self.save_cookies, width=15, state=tk.DISABLED)
self.save_button.pack(side=tk.LEFT, padx=5)
self.import_button = ttk.Button(button_frame, text="导入", command=self.import_cookies, width=15)
self.import_button.pack(side=tk.LEFT, padx=5)
self.close_button = ttk.Button(button_frame, text="关闭", command=self.close_app, width=15)
self.close_button.pack(side=tk.RIGHT, padx=5)
self.status_var = tk.StringVar(value="就绪")
status_frame = ttk.Frame(main_frame)
status_frame.pack(fill=tk.X, pady=10)
ttk.Label(status_frame, text="状态:").pack(side=tk.LEFT, padx=5)
ttk.Label(status_frame, textvariable=self.status_var, foreground="blue").pack(side=tk.LEFT, padx=5)
def open_website(self):
"""打开指定网站"""
url = self.url_var.get().strip()
if not url:
messagebox.showerror("错误", "请输入网址")
return
if not (url.startswith("http://") or url.startswith("https://")):
url = "https://" + url
self.url_var.set(url)
try:
self._cleanup_browser()
self.status_var.set("正在打开浏览器...")
self.root.update()
co = ChromiumOptions()
co.set_browser_path(r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe")
co.set_argument('--no-sandbox')
co.set_argument('--disable-dev-shm-usage')
self.browser = Chromium(co)
self.tab = self.browser.new_tab()
self.status_var.set(f"正在访问: {url}")
self.root.update()
self.tab.get(url)
self.status_var.set("等待页面加载...")
self.root.update()
self.tab.wait.load_start()
self.tab.wait.doc_loaded()
self.status_var.set(f"已打开: {url}")
self.save_button.config(state=tk.NORMAL)
messagebox.showinfo("成功", f"已成功打开网址: {url}\n\n请在浏览器中完成登录操作后,点击'保存'按钮")
except Exception as e:
logger.error(f"打开网站失败: {e}")
messagebox.showerror("错误", f"打开网站失败: {str(e)}")
self.status_var.set("就绪")
self._cleanup_browser()
def save_cookies(self):
"""保存当前的cookie信息,并强制关闭浏览器"""
if not self.browser or not self.tab:
messagebox.showerror("错误", "请先打开网站")
return
try:
self.status_var.set("正在提取cookies...")
self.root.update()
cookies = self.browser.cookies()
logger.info(f"成功提取 {len(cookies)} 个 cookies")
data = {
"cookies": cookies,
"url": self.url_var.get()
}
output_file = "cookie_data.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
self.status_var.set("正在关闭浏览器...")
self.root.update()
self._cleanup_browser()
self.status_var.set(f"数据已保存到: {output_file}")
messagebox.showinfo("成功", f"已成功保存 {len(cookies)} 个cookie到: {output_file}\n\n浏览器已关闭")
except Exception as e:
logger.error(f"保存cookies失败: {e}")
messagebox.showerror("错误", f"保存cookies失败: {str(e)}")
self.status_var.set("就绪")
self._cleanup_browser()
def import_cookies(self):
"""打开浏览器,导入cookie信息,然后打开指定网站"""
try:
output_file = "cookie_data.json"
if not os.path.exists(output_file):
messagebox.showerror("错误", f"文件不存在: {output_file}")
return
self.status_var.set("正在读取cookies...")
self.root.update()
with open(output_file, 'r', encoding='utf-8') as f:
data = json.load(f)
url = data.get("url", "")
cookies = data.get("cookies", [])
if not url:
url = self.url_var.get().strip()
if not url:
messagebox.showerror("错误", "没有有效的网址")
return
if not (url.startswith("http://") or url.startswith("https://")):
url = "https://" + url
self._cleanup_browser()
self.status_var.set("正在打开浏览器...")
self.root.update()
co = ChromiumOptions()
co.set_browser_path(r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe")
co.set_argument('--no-sandbox')
co.set_argument('--disable-dev-shm-usage')
self.browser = Chromium(co)
self.tab = self.browser.new_tab()
self.status_var.set("正在准备环境...")
self.root.update()
self.tab.get("about:blank")
if cookies:
self.status_var.set("正在导入cookies...")
self.root.update()
success_count = 0
for cookie in cookies:
try:
if not cookie.get('name') or not cookie.get('value'):
continue
cookie.setdefault('path', '/')
cookie.setdefault('httpOnly', False)
cookie.setdefault('secure', False)
self.browser.set.cookies([cookie])
success_count += 1
except Exception as e:
logger.error(f"导入cookie失败: {cookie.get('name')} - {e}")
logger.info(f"成功导入 {success_count} 个 cookies")
self.status_var.set(f"正在访问: {url}")
self.root.update()
self.tab.get(url)
self.status_var.set("等待页面加载...")
self.root.update()
self.tab.wait.load_start()
self.tab.wait.doc_loaded()
self.url_var.set(url)
self.status_var.set(f"已导入并打开: {url}")
self.save_button.config(state=tk.NORMAL)
messagebox.showinfo("成功", f"已成功导入 {success_count} 个cookie并打开网址: {url}")
except Exception as e:
logger.error(f"导入cookies失败: {e}")
messagebox.showerror("错误", f"导入cookies失败: {str(e)}")
self.status_var.set("就绪")
self._cleanup_browser()
def close_app(self):
self._cleanup_browser()
self.root.destroy()
def _cleanup_browser(self):
if self.browser:
try:
self.browser.quit()
except Exception as e:
logger.error(f"关闭浏览器失败: {e}")
finally:
self.browser = None
self.tab = None
self.save_button.config(state=tk.DISABLED)
self.status_var.set("就绪")
def main():
root = tk.Tk()
app = CookieExtractorApp(root)
def on_closing():
app._cleanup_browser()
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
if __name__ == "__main__":
main()
+248
View File
@@ -0,0 +1,248 @@
# CookieCloud Client
一个独立的、可复用的CookieCloud服务器客户端Python模块。
## 特性
-**完全独立** - 无外部依赖,仅使用Python标准库
-**类型安全** - 完整的类型提示支持
-**异常处理** - 完善的异常体系
-**易于使用** - 简洁的API设计
-**可扩展** - 支持自定义配置
-**文档完善** - 详细的使用文档和示例
## 安装
`cookiecloud_client` 目录复制到您的项目中即可使用。
## 快速开始
### 基本使用
```python
from cookiecloud_client import CookieCloudClient, CookieConfig
# 创建配置
config = CookieConfig(
server="https://cookiecloud.example.com",
username="your_username",
password="your_password"
)
# 创建客户端
client = CookieCloudClient(config)
# 下载所有cookies
result = client.download()
if result.success:
print(f"下载成功!")
print(f"总域名数: {result.total_domains}")
print(f"总Cookie数: {result.total_cookies}")
print(f"下载耗时: {result.download_time:.2f}")
# 获取指定域名的cookie
cookie_str = result.get_cookie_string("example.com")
print(f"example.com的cookie: {cookie_str}")
else:
print(f"下载失败: {result.error_message}")
```
### 下载指定域名的Cookie
```python
# 下载单个域名
cookie_str = client.download_for_domain("baidu.com")
if cookie_str:
print(f"baidu.com的cookie: {cookie_str}")
# 批量下载多个域名
domains = ["baidu.com", "google.com", "github.com"]
cookies = client.download_for_domains(domains)
for domain, cookie in cookies.items():
print(f"{domain}: {cookie}")
```
### 测试连接
```python
success, message = client.test_connection()
if success:
print("连接成功!")
else:
print(f"连接失败: {message}")
```
## 高级用法
### 自定义配置
```python
config = CookieConfig(
server="https://cookiecloud.example.com",
username="your_username",
password="your_password",
timeout=60, # 超时时间(秒)
verify_ssl=False, # 是否验证SSL证书
ignore_cookies=[ # 忽略的cookie名称
"CookieAutoDeleteBrowsingDataCleanup",
"CookieAutoDeleteCleaningDiscarded"
]
)
```
### 异常处理
```python
from cookiecloud_client import (
CookieCloudClient,
CookieConfig,
CookieCloudError,
ConnectionError,
AuthenticationError,
DataParseError,
NetworkError
)
try:
client = CookieCloudClient(config)
result = client.download()
except AuthenticationError as e:
print(f"认证失败: {e.message}")
print(f"用户名: {e.details.get('username')}")
except ConnectionError as e:
print(f"连接失败: {e.message}")
print(f"服务器: {e.details.get('server')}")
print(f"状态码: {e.details.get('status_code')}")
except DataParseError as e:
print(f"数据解析失败: {e.message}")
except NetworkError as e:
print(f"网络错误: {e.message}")
print(f"原始错误: {e.details.get('original_error')}")
except CookieCloudError as e:
print(f"CookieCloud错误: {e.message}")
```
### 使用Cookie数据对象
```python
result = client.download()
# 获取所有域名
domains = result.get_domains()
print(f"所有域名: {domains}")
# 遍历所有cookie
for domain, collection in result.cookies.items():
print(f"\n域名: {domain}")
for cookie in collection.cookies:
print(f" - {cookie.name}={cookie.value[:20]}...")
print(f" 路径: {cookie.path}")
print(f" 安全: {cookie.secure}")
print(f" HttpOnly: {cookie.http_only}")
```
## API文档
### CookieConfig
配置类,用于存储CookieCloud服务器配置。
**参数:**
- `server` (str): CookieCloud服务器地址
- `username` (str): 用户名
- `password` (str): 密码
- `timeout` (int): 请求超时时间(秒),默认30
- `verify_ssl` (bool): 是否验证SSL证书,默认True
- `ignore_cookies` (List[str]): 忽略的cookie名称列表
### CookieCloudClient
客户端类,用于与CookieCloud服务器交互。
**方法:**
#### `download() -> DownloadResult`
下载所有cookies
**返回:** DownloadResult对象
#### `download_for_domain(domain: str) -> Optional[str]`
下载指定域名的cookie字符串
**参数:**
- `domain` (str): 目标域名
**返回:** cookie字符串或None
#### `download_for_domains(domains: List[str]) -> Dict[str, Optional[str]]`
批量下载多个域名的cookie字符串
**参数:**
- `domains` (List[str]): 目标域名列表
**返回:** {domain: cookie_string}字典
#### `test_connection() -> Tuple[bool, str]`
测试与CookieCloud服务器的连接
**返回:** (是否成功, 消息)元组
### DownloadResult
下载结果类。
**属性:**
- `success` (bool): 是否成功
- `cookies` (Dict[str, CookieCollection]): cookie集合字典
- `error_message` (str): 错误信息
- `total_domains` (int): 总域名数
- `total_cookies` (int): 总cookie数
- `download_time` (float): 下载耗时(秒)
**方法:**
- `get_cookie_string(domain: str) -> Optional[str]`: 获取指定域名的cookie字符串
- `get_domains() -> List[str]`: 获取所有域名列表
## 异常类
### CookieCloudError
基础异常类,所有其他异常都继承自此类。
### ConfigurationError
配置错误异常
### ConnectionError
连接错误异常
### AuthenticationError
认证错误异常
### DataParseError
数据解析错误异常
### NetworkError
网络错误异常
## 依赖
- Python 3.7+
- 仅使用Python标准库
## 许可证
MIT License
## 更新日志
### v1.0.0 (2026-03-02)
- 初始版本发布
- 完整的CookieCloud客户端功能
- 独立模块,无外部依赖
- 完善的异常处理
- 详细的文档和示例
+33
View File
@@ -0,0 +1,33 @@
"""
CookieCloud客户端模块
一个独立的、可复用的CookieCloud服务器客户端
作者: CookieManager Team
版本: 1.0.0
许可证: MIT
"""
from .client import CookieCloudClient
from .exceptions import (
CookieCloudError,
ConnectionError,
AuthenticationError,
DataParseError,
ConfigurationError,
NetworkError
)
from .models import CookieData, CookieConfig
from .version import __version__
__all__ = [
'CookieCloudClient',
'CookieCloudError',
'ConnectionError',
'AuthenticationError',
'DataParseError',
'ConfigurationError',
'NetworkError',
'CookieData',
'CookieConfig',
'__version__'
]
+347
View File
@@ -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
+259
View File
@@ -0,0 +1,259 @@
"""
CookieCloud客户端使用示例
演示如何使用cookiecloud_client模块
"""
import sys
import os
# 添加父目录到路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 示例1: 基本使用
def example_basic_usage():
"""基本使用示例"""
from cookiecloud_client import CookieCloudClient, CookieConfig
print("=" * 60)
print("示例1: 基本使用")
print("=" * 60)
# 创建配置
config = CookieConfig(
server="https://movie-pilot.org/cookiecloud",
username="hu6n2vcUmzpu7mqUN2rVCg",
password="jtDM5dV9AyqXkZdQVeA9f6"
)
# 创建客户端
client = CookieCloudClient(config)
# 测试连接
success, message = client.test_connection()
print(f"连接测试: {message}")
if success:
# 下载所有cookies
result = client.download()
if result.success:
print(f"\n✓ 下载成功!")
print(f" 总域名数: {result.total_domains}")
print(f" 总Cookie数: {result.total_cookies}")
print(f" 下载耗时: {result.download_time:.2f}")
# 显示前5个域名
domains = result.get_domains()[:5]
print(f"\n前5个域名:")
for idx, domain in enumerate(domains, 1):
cookie_str = result.get_cookie_string(domain)
preview = cookie_str[:50] + "..." if len(cookie_str) > 50 else cookie_str
print(f" {idx}. {domain}: {preview}")
else:
print(f"\n✗ 下载失败: {result.error_message}")
# 示例2: 下载指定域名的Cookie
def example_download_specific_domain():
"""下载指定域名示例"""
from cookiecloud_client import CookieCloudClient, CookieConfig
print("\n" + "=" * 60)
print("示例2: 下载指定域名的Cookie")
print("=" * 60)
config = CookieConfig(
server="https://movie-pilot.org/cookiecloud",
username="hu6n2vcUmzpu7mqUN2rVCg",
password="jtDM5dV9AyqXkZdQVeA9f6"
)
client = CookieCloudClient(config)
# 下载单个域名
domain = "baidu.com"
cookie_str = client.download_for_domain(domain)
if cookie_str:
print(f"{domain}的Cookie:")
print(f" {cookie_str[:100]}...")
else:
print(f"✗ 未找到{domain}的Cookie")
# 示例3: 批量下载多个域名
def example_download_multiple_domains():
"""批量下载示例"""
from cookiecloud_client import CookieCloudClient, CookieConfig
print("\n" + "=" * 60)
print("示例3: 批量下载多个域名")
print("=" * 60)
config = CookieConfig(
server="https://movie-pilot.org/cookiecloud",
username="hu6n2vcUmzpu7mqUN2rVCg",
password="jtDM5dV9AyqXkZdQVeA9f6"
)
client = CookieCloudClient(config)
# 批量下载
domains = ["baidu.com", "github.com", "google.com", "bing.com"]
cookies = client.download_for_domains(domains)
print("批量下载结果:")
for domain, cookie_str in cookies.items():
if cookie_str:
preview = cookie_str[:50] + "..."
print(f"{domain}: {preview}")
else:
print(f"{domain}: 未找到Cookie")
# 示例4: 异常处理
def example_error_handling():
"""异常处理示例"""
from cookiecloud_client import (
CookieCloudClient,
CookieConfig,
CookieCloudError,
ConnectionError,
AuthenticationError,
NetworkError
)
print("\n" + "=" * 60)
print("示例4: 异常处理")
print("=" * 60)
try:
# 使用错误的凭据
config = CookieConfig(
server="https://movie-pilot.org/cookiecloud",
username="wrong_user",
password="wrong_pass"
)
client = CookieCloudClient(config)
result = client.download()
except AuthenticationError as e:
print(f"✗ 认证失败: {e.message}")
print(f" 用户名: {e.details.get('username')}")
except ConnectionError as e:
print(f"✗ 连接失败: {e.message}")
print(f" 服务器: {e.details.get('server')}")
print(f" 状态码: {e.details.get('status_code')}")
except NetworkError as e:
print(f"✗ 网络错误: {e.message}")
print(f" 原始错误: {e.details.get('original_error')}")
except CookieCloudError as e:
print(f"✗ CookieCloud错误: {e.message}")
# 示例5: 自定义配置
def example_custom_config():
"""自定义配置示例"""
from cookiecloud_client import CookieCloudClient, CookieConfig
print("\n" + "=" * 60)
print("示例5: 自定义配置")
print("=" * 60)
# 自定义配置
config = CookieConfig(
server="https://movie-pilot.org/cookiecloud",
username="hu6n2vcUmzpu7mqUN2rVCg",
password="jtDM5dV9AyqXkZdQVeA9f6",
timeout=60, # 60秒超时
verify_ssl=False, # 不验证SSL证书
ignore_cookies=[ # 忽略的cookie
"CookieAutoDeleteBrowsingDataCleanup",
"CookieAutoDeleteCleaningDiscarded",
"_ga", # 忽略Google Analytics
]
)
client = CookieCloudClient(config)
print(f"配置信息:")
print(f" 服务器: {config.server}")
print(f" 用户名: {config.username}")
print(f" 超时时间: {config.timeout}")
print(f" 验证SSL: {config.verify_ssl}")
print(f" 忽略Cookie: {len(config.ignore_cookies)}")
result = client.download()
if result.success:
print(f"\n✓ 下载成功")
print(f" 总域名数: {result.total_domains}")
print(f" 总Cookie数: {result.total_cookies}")
# 示例6: 使用Cookie数据对象
def example_cookie_data_objects():
"""使用Cookie数据对象示例"""
from cookiecloud_client import CookieCloudClient, CookieConfig
print("\n" + "=" * 60)
print("示例6: 使用Cookie数据对象")
print("=" * 60)
config = CookieConfig(
server="https://movie-pilot.org/cookiecloud",
username="hu6n2vcUmzpu7mqUN2rVCg",
password="jtDM5dV9AyqXkZdQVeA9f6"
)
client = CookieCloudClient(config)
result = client.download()
if result.success:
# 获取第一个域名的详细信息
first_domain = result.get_domains()[0]
collection = result.cookies.get(first_domain)
if collection:
print(f"域名: {collection.domain}")
print(f"Cookie数量: {len(collection.cookies)}")
print(f"\nCookie详情:")
for idx, cookie in enumerate(collection.cookies[:3], 1):
print(f" {idx}. {cookie.name}")
print(f" 值: {cookie.value[:30]}...")
print(f" 路径: {cookie.path}")
print(f" 安全: {cookie.secure}")
print(f" HttpOnly: {cookie.http_only}")
print()
# 主函数
def main():
"""运行所有示例"""
print("\n")
print("" + "=" * 58 + "")
print("" + " " * 15 + "CookieCloud客户端使用示例" + " " * 17 + "")
print("" + "=" * 58 + "")
try:
example_basic_usage()
example_download_specific_domain()
example_download_multiple_domains()
example_error_handling()
example_custom_config()
example_cookie_data_objects()
print("\n" + "=" * 60)
print("所有示例执行完成!")
print("=" * 60)
except Exception as e:
print(f"\n示例执行出错: {str(e)}")
if __name__ == "__main__":
main()
+65
View File
@@ -0,0 +1,65 @@
"""
CookieCloud客户端异常类
"""
class CookieCloudError(Exception):
"""CookieCloud基础异常类"""
def __init__(self, message: str, details: dict = None):
self.message = message
self.details = details or {}
super().__init__(self.message)
def __str__(self):
if self.details:
return f"{self.message} - 详情: {self.details}"
return self.message
class ConfigurationError(CookieCloudError):
"""配置错误异常"""
def __init__(self, message: str, config_key: str = None):
details = {'config_key': config_key} if config_key else {}
super().__init__(message, details)
class ConnectionError(CookieCloudError):
"""连接错误异常"""
def __init__(self, message: str, server: str = None, status_code: int = None):
details = {}
if server:
details['server'] = server
if status_code:
details['status_code'] = status_code
super().__init__(message, details)
class AuthenticationError(CookieCloudError):
"""认证错误异常"""
def __init__(self, message: str, username: str = None):
details = {'username': username} if username else {}
super().__init__(message, details)
class DataParseError(CookieCloudError):
"""数据解析错误异常"""
def __init__(self, message: str, raw_data: str = None):
details = {}
if raw_data:
details['raw_data_length'] = len(raw_data)
super().__init__(message, details)
class NetworkError(CookieCloudError):
"""网络错误异常"""
def __init__(self, message: str, original_error: Exception = None):
details = {}
if original_error:
details['original_error'] = str(original_error)
super().__init__(message, details)
+126
View File
@@ -0,0 +1,126 @@
"""
CookieCloud数据模型
"""
from dataclasses import dataclass, field
from typing import Optional, Dict, List
from datetime import datetime
@dataclass
class CookieConfig:
"""CookieCloud配置"""
server: str
username: str
password: str
timeout: int = 30
verify_ssl: bool = True
ignore_cookies: List[str] = field(default_factory=lambda: [
"CookieAutoDeleteBrowsingDataCleanup",
"CookieAutoDeleteCleaningDiscarded"
])
def __post_init__(self):
"""验证配置参数"""
if not self.server:
raise ValueError("服务器地址不能为空")
if not self.username:
raise ValueError("用户名不能为空")
if not self.password:
raise ValueError("密码不能为空")
if not self.server.startswith(('http://', 'https://')):
self.server = f"https://{self.server}"
@dataclass
class CookieData:
"""Cookie数据"""
domain: str
name: str
value: str
path: str = "/"
secure: bool = False
http_only: bool = False
expiry: Optional[datetime] = None
def to_dict(self) -> Dict:
"""转换为字典"""
return {
'domain': self.domain,
'name': self.name,
'value': self.value,
'path': self.path,
'secure': self.secure,
'httpOnly': self.http_only,
'expiry': self.expiry.isoformat() if self.expiry else None
}
@classmethod
def from_dict(cls, data: Dict) -> 'CookieData':
"""从字典创建"""
expiry = None
if data.get('expiry'):
try:
expiry = datetime.fromisoformat(data['expiry'])
except (ValueError, TypeError):
pass
return cls(
domain=data.get('domain', ''),
name=data.get('name', ''),
value=data.get('value', ''),
path=data.get('path', '/'),
secure=data.get('secure', False),
http_only=data.get('httpOnly', False),
expiry=expiry
)
@dataclass
class CookieCollection:
"""Cookie集合"""
domain: str
cookies: List[CookieData] = field(default_factory=list)
def to_cookie_string(self, ignore_list: List[str] = None) -> str:
"""转换为cookie字符串"""
ignore_list = ignore_list or []
cookie_parts = []
for cookie in self.cookies:
if cookie.name not in ignore_list:
cookie_parts.append(f"{cookie.name}={cookie.value}")
return ";".join(cookie_parts)
def add_cookie(self, cookie: CookieData):
"""添加cookie"""
self.cookies.append(cookie)
def get_cookie_by_name(self, name: str) -> Optional[CookieData]:
"""根据名称获取cookie"""
for cookie in self.cookies:
if cookie.name == name:
return cookie
return None
@dataclass
class DownloadResult:
"""下载结果"""
success: bool
cookies: Dict[str, CookieCollection] = field(default_factory=dict)
error_message: str = ""
total_domains: int = 0
total_cookies: int = 0
download_time: float = 0.0
def get_cookie_string(self, domain: str) -> Optional[str]:
"""获取指定域名的cookie字符串"""
collection = self.cookies.get(domain)
if collection:
return collection.to_cookie_string()
return None
def get_domains(self) -> List[str]:
"""获取所有域名列表"""
return list(self.cookies.keys())
+14
View File
@@ -0,0 +1,14 @@
# CookieCloud客户端依赖项
# 此模块完全独立,仅使用Python标准库
# Python版本要求
Python>=3.7
# 无外部依赖
# 仅使用Python标准库模块:
# - json
# - urllib
# - dataclasses
# - typing
# - datetime
# - unittest (用于测试)
+54
View File
@@ -0,0 +1,54 @@
"""
CookieCloud客户端安装脚本
"""
from setuptools import setup, find_packages
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="cookiecloud-client",
version="1.0.0",
author="CookieManager Team",
author_email="support@example.com",
description="一个独立的、可复用的CookieCloud服务器客户端",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/example/cookiecloud-client",
packages=find_packages(),
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
python_requires=">=3.7",
install_requires=[
# 无外部依赖,仅使用Python标准库
],
extras_require={
"dev": [
"pytest>=6.0",
"pytest-cov>=2.0",
"black>=21.0",
"flake8>=3.9",
],
},
entry_points={
"console_scripts": [
"cookiecloud-client=cookiecloud_client.cli:main",
],
},
project_urls={
"Bug Reports": "https://github.com/example/cookiecloud-client/issues",
"Source": "https://github.com/example/cookiecloud-client",
"Documentation": "https://github.com/example/cookiecloud-client#readme",
},
)
+295
View File
@@ -0,0 +1,295 @@
"""
CookieCloud客户端单元测试
"""
import unittest
from unittest.mock import Mock, patch, MagicMock
import json
from cookiecloud_client import (
CookieCloudClient,
CookieConfig,
CookieCloudError,
ConfigurationError,
ConnectionError,
AuthenticationError,
DataParseError,
NetworkError
)
class TestCookieConfig(unittest.TestCase):
"""测试CookieConfig配置类"""
def test_valid_config(self):
"""测试有效配置"""
config = CookieConfig(
server="https://example.com",
username="user",
password="pass"
)
self.assertEqual(config.server, "https://example.com")
self.assertEqual(config.username, "user")
self.assertEqual(config.password, "pass")
self.assertEqual(config.timeout, 30)
self.assertTrue(config.verify_ssl)
def test_config_with_custom_params(self):
"""测试自定义参数配置"""
config = CookieConfig(
server="https://example.com",
username="user",
password="pass",
timeout=60,
verify_ssl=False,
ignore_cookies=["test_cookie"]
)
self.assertEqual(config.timeout, 60)
self.assertFalse(config.verify_ssl)
self.assertEqual(config.ignore_cookies, ["test_cookie"])
def test_config_auto_add_protocol(self):
"""测试自动添加协议"""
config = CookieConfig(
server="example.com",
username="user",
password="pass"
)
self.assertTrue(config.server.startswith("https://"))
def test_config_empty_server(self):
"""测试空服务器地址"""
with self.assertRaises(ValueError):
CookieConfig(server="", username="user", password="pass")
def test_config_empty_username(self):
"""测试空用户名"""
with self.assertRaises(ValueError):
CookieConfig(server="https://example.com", username="", password="pass")
def test_config_empty_password(self):
"""测试空密码"""
with self.assertRaises(ValueError):
CookieConfig(server="https://example.com", username="user", password="")
class TestCookieCloudClient(unittest.TestCase):
"""测试CookieCloudClient客户端类"""
def setUp(self):
"""测试前准备"""
self.config = CookieConfig(
server="https://test.example.com",
username="testuser",
password="testpass"
)
self.client = CookieCloudClient(self.config)
def test_client_initialization(self):
"""测试客户端初始化"""
self.assertIsInstance(self.client.config, CookieConfig)
self.assertIsNone(self.client.last_download_time)
self.assertEqual(self.client.download_count, 0)
def test_client_invalid_config(self):
"""测试无效配置"""
with self.assertRaises(ConfigurationError):
CookieCloudClient("invalid_config")
@patch('urllib.request.urlopen')
def test_download_success(self, mock_urlopen):
"""测试成功下载"""
mock_response = Mock()
mock_response.status = 200
mock_response.read.return_value = json.dumps({
"cookie_data": {
"test.com": [
{
"domain": "test.com",
"name": "session",
"value": "test_value",
"path": "/",
"secure": False,
"httpOnly": False
}
]
}
}).encode('utf-8')
mock_urlopen.return_value = mock_response
result = self.client.download()
self.assertTrue(result.success)
self.assertEqual(result.total_domains, 1)
self.assertEqual(result.total_cookies, 1)
self.assertIsNotNone(self.client.last_download_time)
self.assertEqual(self.client.download_count, 1)
@patch('urllib.request.urlopen')
def test_download_authentication_error(self, mock_urlopen):
"""测试认证失败"""
import urllib.error
mock_urlopen.side_effect = urllib.error.HTTPError(
url="https://test.example.com/get/testuser",
code=401,
msg="Unauthorized",
hdrs={},
fp=None
)
with self.assertRaises(AuthenticationError):
self.client.download()
@patch('urllib.request.urlopen')
def test_download_connection_error(self, mock_urlopen):
"""测试连接错误"""
import urllib.error
mock_urlopen.side_effect = urllib.error.HTTPError(
url="https://test.example.com/get/testuser",
code=404,
msg="Not Found",
hdrs={},
fp=None
)
with self.assertRaises(ConnectionError):
self.client.download()
@patch('urllib.request.urlopen')
def test_download_network_error(self, mock_urlopen):
"""测试网络错误"""
import urllib.error
mock_urlopen.side_effect = urllib.error.URLError("Connection refused")
with self.assertRaises(NetworkError):
self.client.download()
@patch('urllib.request.urlopen')
def test_download_empty_data(self, mock_urlopen):
"""测试空数据"""
mock_response = Mock()
mock_response.status = 200
mock_response.read.return_value = json.dumps({}).encode('utf-8')
mock_urlopen.return_value = mock_response
with self.assertRaises(DataParseError):
self.client.download()
@patch('urllib.request.urlopen')
def test_test_connection_success(self, mock_urlopen):
"""测试连接测试成功"""
mock_response = Mock()
mock_response.status = 200
mock_response.read.return_value = json.dumps({
"cookie_data": {}
}).encode('utf-8')
mock_urlopen.return_value = mock_response
success, message = self.client.test_connection()
self.assertTrue(success)
self.assertEqual(message, "连接成功")
@patch('urllib.request.urlopen')
def test_test_connection_failure(self, mock_urlopen):
"""测试连接测试失败"""
import urllib.error
mock_urlopen.side_effect = urllib.error.HTTPError(
url="https://test.example.com/get/testuser",
code=401,
msg="Unauthorized",
hdrs={},
fp=None
)
success, message = self.client.test_connection()
self.assertFalse(success)
self.assertIn("认证失败", message)
@patch('urllib.request.urlopen')
def test_download_for_domain(self, mock_urlopen):
"""测试下载指定域名"""
mock_response = Mock()
mock_response.status = 200
mock_response.read.return_value = json.dumps({
"cookie_data": {
"test.com": [
{
"domain": "test.com",
"name": "session",
"value": "test_value",
"path": "/",
"secure": False,
"httpOnly": False
}
]
}
}).encode('utf-8')
mock_urlopen.return_value = mock_response
cookie_str = self.client.download_for_domain("test.com")
self.assertIsNotNone(cookie_str)
self.assertIn("session=test_value", cookie_str)
@patch('urllib.request.urlopen')
def test_download_for_domains(self, mock_urlopen):
"""测试批量下载多个域名"""
mock_response = Mock()
mock_response.status = 200
mock_response.read.return_value = json.dumps({
"cookie_data": {
"test1.com": [
{"domain": "test1.com", "name": "cookie1", "value": "value1", "path": "/"}
],
"test2.com": [
{"domain": "test2.com", "name": "cookie2", "value": "value2", "path": "/"}
]
}
}).encode('utf-8')
mock_urlopen.return_value = mock_response
domains = ["test1.com", "test2.com", "test3.com"]
cookies = self.client.download_for_domains(domains)
self.assertEqual(len(cookies), 3)
self.assertIn("cookie1=value1", cookies["test1.com"])
self.assertIn("cookie2=value2", cookies["test2.com"])
self.assertIsNone(cookies["test3.com"])
class TestExceptions(unittest.TestCase):
"""测试异常类"""
def test_cookie_cloud_error(self):
"""测试基础异常"""
error = CookieCloudError("测试错误", {"key": "value"})
self.assertEqual(error.message, "测试错误")
self.assertEqual(error.details, {"key": "value"})
self.assertIn("测试错误", str(error))
def test_configuration_error(self):
"""测试配置错误"""
error = ConfigurationError("配置错误", config_key="server")
self.assertEqual(error.message, "配置错误")
self.assertEqual(error.details["config_key"], "server")
def test_connection_error(self):
"""测试连接错误"""
error = ConnectionError("连接失败", server="example.com", status_code=404)
self.assertEqual(error.message, "连接失败")
self.assertEqual(error.details["server"], "example.com")
self.assertEqual(error.details["status_code"], 404)
def test_authentication_error(self):
"""测试认证错误"""
error = AuthenticationError("认证失败", username="testuser")
self.assertEqual(error.message, "认证失败")
self.assertEqual(error.details["username"], "testuser")
if __name__ == '__main__':
unittest.main()
+8
View File
@@ -0,0 +1,8 @@
"""
版本信息
"""
__version__ = '1.0.0'
__author__ = 'CookieManager Team'
__email__ = 'support@example.com'
__license__ = 'MIT'
+12 -1
View File
@@ -1 +1,12 @@
{}
[
{
"name": "language",
"value": "zh-CN",
"domain": "192.168.8.156"
},
{
"name": "fnos-token",
"value": "sxRTKZf7p2lA4txCiYB1dvB0xT+sVmDkrmxaX3L61a4=",
"domain": "192.168.8.156"
}
]
-345
View File
File diff suppressed because one or more lines are too long
+103 -142
View File
@@ -1,7 +1,5 @@
"""Cookie 监控主程序"""
import json
import os
import logging
from datetime import datetime
from cookie_cloud import CookieCloud
from notifier import IYUUNotifier, FailureTracker
@@ -15,234 +13,197 @@ class CookieMonitor:
"""Cookie 监控器"""
def __init__(self, config_file: str = "config.json"):
"""
初始化 Cookie 监控器
Args:
config_file: 配置文件路径
"""
self.config_file = config_file
self.config = self._load_config()
self.cookie_manager = CookieManager("cookies.json")
self.failure_tracker = FailureTracker("state.json")
def _load_config(self) -> dict:
"""加载配置文件"""
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"加载配置文件失败: {e}")
logger.error(f"加载配置文件失败: {e}")
return {}
def _log(self, message: str):
"""记录日志"""
logger.info(message)
def _get_domain_from_url(self, url: str) -> str:
"""从 URL 中提取域名"""
from urllib.parse import urlparse
parsed = urlparse(url)
return parsed.netloc
return urlparse(url).netloc
def process_user(self, user_config: dict):
"""
处理单个用户的所有网站
Args:
user_config: 用户配置
处理单个用户:一次性导入所有cookies,然后验证各网站登录状态
"""
user_name = user_config.get('name', '未知用户')
self._log(f"开始处理用户: {user_name}")
logger.info(f"{'='*20} 开始处理用户: {user_name} {'='*20}")
# 获取用户配置
cookie_cloud_config = user_config.get('cookie_cloud', {})
notification_config = user_config.get('notification', {})
browser_config = user_config.get('browser', {})
websites = user_config.get('websites', [])
# 初始化爱语飞飞通知
if not websites:
logger.info(f"用户 {user_name} 没有配置网站,跳过")
return
iyuu_token = notification_config.get('iyuu_token', '')
max_fail_count = notification_config.get('max_fail_count', 3)
notifier = IYUUNotifier(iyuu_token) if iyuu_token else None
# 初始化 Cookie Cloud
cookie_cloud = CookieCloud(
api_url=cookie_cloud_config.get('api_url', ''),
uuid=cookie_cloud_config.get('uuid', ''),
password=cookie_cloud_config.get('password', '')
)
# 初始化浏览器
logger.info(f"步骤1: 从 Cookie Cloud 获取所有 cookies")
all_cookies = cookie_cloud.get_cookies()
total_domains = len(all_cookies)
total_cookies = sum(len(c) for c in all_cookies.values())
logger.info(f"获取到 {total_domains} 个域名,共 {total_cookies} 个 cookie")
browser = BrowserLogin(
browser_type=browser_config.get('type', 'edge'),
headless=browser_config.get('headless', False)
)
results = []
try:
# 处理每个网站
logger.info(f"步骤2: 一次性导入所有 cookies 到浏览器")
all_cookies_list = []
for cookies in all_cookies.values():
all_cookies_list.extend(cookies)
import_success = browser.import_all_cookies(all_cookies_list)
if import_success:
logger.info(f"成功导入 {len(all_cookies_list)} 个 cookie")
else:
logger.warning(f"部分 cookie 导入失败")
logger.info(f"步骤3: 依次验证各网站登录状态")
for website in websites:
self.process_website(
result = self.process_website(
user_name=user_name,
website_config=website,
cookie_cloud=cookie_cloud,
notifier=notifier,
max_fail_count=max_fail_count,
browser=browser
)
finally:
# 关闭浏览器
browser.close()
results.append(result)
self._log(f"用户 {user_name} 处理完成")
if not result['success']:
self.failure_tracker.check_and_notify(
user_name=user_name,
website_name=result['name'],
max_fail_count=max_fail_count,
notifier=notifier,
error_msg=result['error']
)
else:
self.failure_tracker.reset_fail_count(user_name, result['name'])
self._print_summary(user_name, results)
finally:
browser.close()
def process_website(
self,
user_name: str,
website_config: dict,
cookie_cloud: CookieCloud,
notifier: IYUUNotifier,
max_fail_count: int,
browser: BrowserLogin
):
) -> dict:
"""
处理单个网站
验证网站登录状态(cookies已预先导入)
Args:
user_name: 用户名
website_config: 网站配置
cookie_cloud: Cookie Cloud 客户端
notifier: 爱语飞飞通知客户端
max_fail_count: 最大失败次数
browser: 浏览器客户端
Returns:
dict: {'name': 网站名, 'url': URL, 'success': 是否成功, 'error': 错误信息}
"""
website_name = website_config.get('name', '未知网站')
url = website_config.get('url', '')
check_selector = website_config.get('login_check_selector', '')
success_text = website_config.get('success_text', '')
self._log(f"处理网站: {user_name} - {website_name}")
logger.info(f"\n验证网站: {website_name}")
logger.info(f" URL: {url}")
# 获取域名
domain = self._get_domain_from_url(url)
result = {
'name': website_name,
'url': url,
'success': False,
'error': ''
}
if not url:
result['error'] = 'URL 未配置'
logger.error(f" 失败: {result['error']}")
return result
try:
# 第一步:使用本地 cookie 登录
self._log(f"尝试使用本地 cookie 登录: {website_name}")
local_cookies = self.cookie_manager.get_cookies(user_name, website_name)
if local_cookies:
login_success = browser.login_with_cookies(
url=url,
cookies=local_cookies,
check_selector=check_selector,
success_text=success_text
)
if login_success:
self._log(f"本地 cookie 登录成功: {website_name}")
# 刷新页面并保存新的 cookie
new_cookies = browser.refresh_and_save_cookies(url)
if new_cookies:
self.cookie_manager.save_cookies(user_name, website_name, new_cookies)
self._log(f"已更新 cookie: {website_name}")
# 重置失败计数
self.failure_tracker.reset_fail_count(user_name, website_name)
return
self._log(f"本地 cookie 登录失败: {website_name}")
# 第二步:从 Cookie Cloud 获取 cookie 并重试
self._log(f"从 Cookie Cloud 获取 cookie: {website_name}")
cloud_cookies = cookie_cloud.get_cookies_for_domain(domain)
if cloud_cookies:
login_success = browser.login_with_cookies(
url=url,
cookies=cloud_cookies,
check_selector=check_selector,
success_text=success_text
)
if login_success:
self._log(f"Cookie Cloud 登录成功: {website_name}")
# 刷新页面并保存新的 cookie
new_cookies = browser.refresh_and_save_cookies(url)
if new_cookies:
self.cookie_manager.save_cookies(user_name, website_name, new_cookies)
self._log(f"已更新 cookie: {website_name}")
# 重置失败计数
self.failure_tracker.reset_fail_count(user_name, website_name)
return
self._log(f"Cookie Cloud 登录失败: {website_name}")
else:
self._log(f"未从 Cookie Cloud 获取到 cookie: {website_name}")
# 第三步:登录失败,增加失败计数并检查是否需要通知
self._log(f"登录失败: {website_name}")
error_msg = f"{website_name} 登录失败,请检查 Cookie Cloud 是否有最新的 cookie"
should_stop = self.failure_tracker.check_and_notify(
user_name=user_name,
website_name=website_name,
max_fail_count=max_fail_count,
notifier=notifier,
error_msg=error_msg
login_success = browser.verify_login(
url=url,
check_selector=check_selector,
success_text=success_text
)
if should_stop:
self._log(f"已达到最大失败次数,已发送通知并重置计数: {website_name}")
if login_success:
result['success'] = True
logger.info(f" ✓ 登录验证成功")
new_cookies = browser.refresh_and_save_cookies(url)
if new_cookies:
self.cookie_manager.save_cookies(user_name, website_name, new_cookies)
logger.info(f" 已保存 {len(new_cookies)} 个新 cookie")
else:
fail_count = self.failure_tracker.get_fail_count(user_name, website_name)
self._log(f"当前失败次数: {fail_count}/{max_fail_count}")
result['error'] = '登录状态验证失败(cookies可能已过期或选择器配置错误)'
logger.warning(f"{result['error']}")
except Exception as e:
self._log(f"处理网站 {website_name} 时发生异常: {e}")
error_msg = f"{website_name} 处理异常: {str(e)}"
result['error'] = f'处理异常: {str(e)}'
logger.error(f" 异常: {e}")
should_stop = self.failure_tracker.check_and_notify(
user_name=user_name,
website_name=website_name,
max_fail_count=max_fail_count,
notifier=notifier,
error_msg=error_msg
)
return result
def _print_summary(self, user_name: str, results: list):
"""打印处理结果汇总"""
success_count = sum(1 for r in results if r['success'])
fail_count = len(results) - success_count
logger.info(f"\n{'='*20} 用户 {user_name} 处理结果汇总 {'='*20}")
logger.info(f"总网站数: {len(results)}")
logger.info(f"成功: {success_count}, 失败: {fail_count}")
if fail_count > 0:
logger.info("\n失败详情:")
for r in results:
if not r['success']:
logger.info(f" - {r['name']}: {r['error']}")
def run(self):
"""运行监控"""
self._log("===== Cookie 监控开始 =====")
logger.info("="*50)
logger.info("Cookie 监控开始")
logger.info(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
logger.info("="*50)
users = self.config.get('users', [])
if not users:
self._log("未找到用户配置")
logger.error("未找到用户配置")
return
success_count = 0
fail_count = 0
for user_config in users:
try:
self.process_user(user_config)
success_count += 1
except Exception as e:
self._log(f"处理用户失败: {e}")
fail_count += 1
logger.error(f"处理用户失败: {e}")
self._log(f"===== Cookie 监控结束 =====")
self._log(f"成功处理: {success_count} 个用户,失败: {fail_count} 个用户")
logger.info("\n" + "="*50)
logger.info("Cookie 监控结束")
logger.info("="*50)
def main():
"""主函数"""
# 配置日志
setup_logging()
monitor = CookieMonitor()
monitor.run()
-3
View File
@@ -1,3 +0,0 @@
{
"用户A_网站A": 2
}