Compare commits
4
Commits
master
..
918ac92230
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
918ac92230 | ||
|
|
c3d07f7325 | ||
|
|
894acc3191 | ||
|
|
0fbbbce2ee |
+43
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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客户端功能
|
||||
- 独立模块,无外部依赖
|
||||
- 完善的异常处理
|
||||
- 详细的文档和示例
|
||||
@@ -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__'
|
||||
]
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -0,0 +1,14 @@
|
||||
# CookieCloud客户端依赖项
|
||||
# 此模块完全独立,仅使用Python标准库
|
||||
|
||||
# Python版本要求
|
||||
Python>=3.7
|
||||
|
||||
# 无外部依赖
|
||||
# 仅使用Python标准库模块:
|
||||
# - json
|
||||
# - urllib
|
||||
# - dataclasses
|
||||
# - typing
|
||||
# - datetime
|
||||
# - unittest (用于测试)
|
||||
@@ -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",
|
||||
},
|
||||
)
|
||||
@@ -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()
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
版本信息
|
||||
"""
|
||||
|
||||
__version__ = '1.0.0'
|
||||
__author__ = 'CookieManager Team'
|
||||
__email__ = 'support@example.com'
|
||||
__license__ = 'MIT'
|
||||
+12
-1
@@ -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
File diff suppressed because one or more lines are too long
+103
-142
@@ -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()
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"用户A_网站A": 2
|
||||
}
|
||||
Reference in New Issue
Block a user