update
This commit is contained in:
+121
-10
@@ -129,6 +129,99 @@ class BrowserLogin:
|
|||||||
logger.error(f"检查登录状态失败: {e}")
|
logger.error(f"检查登录状态失败: {e}")
|
||||||
return False
|
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")
|
||||||
|
for cookie in cookies:
|
||||||
|
try:
|
||||||
|
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')
|
||||||
|
|
||||||
|
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(
|
def login_with_cookies(
|
||||||
self,
|
self,
|
||||||
url: str,
|
url: str,
|
||||||
@@ -152,20 +245,38 @@ class BrowserLogin:
|
|||||||
self._create_browser()
|
self._create_browser()
|
||||||
|
|
||||||
try:
|
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)
|
self.tab.get(url)
|
||||||
|
|
||||||
# 添加 cookies
|
|
||||||
for cookie in cookies:
|
|
||||||
self.tab.set.cookies(cookie)
|
|
||||||
|
|
||||||
# 刷新页面
|
|
||||||
self.tab.refresh()
|
|
||||||
|
|
||||||
# 等待页面加载
|
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
# 检查登录状态
|
|
||||||
element = self.tab.ele(check_selector, timeout=10)
|
element = self.tab.ele(check_selector, timeout=10)
|
||||||
|
|
||||||
if element and element.text and success_text in element.text:
|
if element and element.text and success_text in element.text:
|
||||||
|
|||||||
+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": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+111
-118
@@ -1,11 +1,11 @@
|
|||||||
"""Cookie Cloud 模块 - 从 Cookie Cloud 服务器获取并解密 cookie 数据"""
|
"""Cookie Cloud 模块 - 从 Cookie Cloud 服务器获取 cookie 数据"""
|
||||||
import hashlib
|
|
||||||
import json
|
import json
|
||||||
|
import hashlib
|
||||||
import base64
|
import base64
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Tuple
|
||||||
import requests
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
from Crypto.Util.Padding import unpad
|
|
||||||
from applogger import get_logger
|
from applogger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -27,77 +27,82 @@ class CookieCloud:
|
|||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
def decrypt(self, encrypted_data: str) -> Dict:
|
def _get_crypt_key(self) -> bytes:
|
||||||
"""
|
"""生成加密密钥"""
|
||||||
解密 Cookie Cloud 数据
|
combined_string = f"{self.uuid}-{self.password}"
|
||||||
|
return hashlib.md5(combined_string.encode('utf-8')).hexdigest()[:16].encode("utf-8")
|
||||||
|
|
||||||
Args:
|
def _bytes_to_key(self, data: bytes, salt: bytes, output: int = 48) -> bytes:
|
||||||
encrypted_data: 加密的数据字符串
|
"""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:
|
def _decrypt(self, encrypted: str, passphrase: bytes) -> bytes:
|
||||||
解密后的数据字典,包含 cookie_data 和 local_storage_data
|
"""解密数据"""
|
||||||
"""
|
encrypted_bytes = base64.b64decode(encrypted)
|
||||||
# 生成解密密钥: md5(uuid+password) 取前 16 个字符
|
assert encrypted_bytes.startswith(b"Salted__"), "Invalid encrypted data format"
|
||||||
key_str = f"{self.uuid}{self.password}"
|
salt = encrypted_bytes[8:16]
|
||||||
key = hashlib.md5(key_str.encode()).hexdigest()[:16].encode()
|
key_iv = self._bytes_to_key(passphrase, salt, 32 + 16)
|
||||||
print(f"生成的密钥: {key}")
|
key = key_iv[:32]
|
||||||
print(f"密钥长度: {len(key)}")
|
iv = key_iv[32:]
|
||||||
|
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||||
# 解码 Base64
|
decrypted_padded = aes.decrypt(encrypted_bytes[16:])
|
||||||
encrypted = base64.b64decode(encrypted_data)
|
padding_length = decrypted_padded[-1]
|
||||||
print(f"Base64 解码后长度: {len(encrypted)}")
|
if isinstance(padding_length, str):
|
||||||
print(f"解码后前 20 字节: {encrypted[:20]}")
|
padding_length = ord(padding_length)
|
||||||
|
return decrypted_padded[:-padding_length]
|
||||||
# 检查是否有盐值(OpenSSL 格式)
|
|
||||||
if encrypted.startswith(b'Salted__'):
|
def _download_data(self) -> Tuple[Optional[Dict], str]:
|
||||||
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]}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 移除 PKCS#7 填充
|
url = f"{self.api_url}/get/{self.uuid}"
|
||||||
decrypted = unpad(decrypted, AES.block_size)
|
request = urllib.request.Request(
|
||||||
print(f"移除填充后长度: {len(decrypted)}")
|
url,
|
||||||
print(f"移除填充后前 100 字节: {decrypted[:100]}")
|
headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'CookieCloud-Client/1.0'
|
||||||
|
},
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
# 尝试解析 JSON
|
response = urllib.request.urlopen(request, timeout=30)
|
||||||
result = json.loads(decrypted.decode('utf-8'))
|
|
||||||
print("JSON 解析成功")
|
if response.status != 200:
|
||||||
return result
|
return None, f"服务器返回错误状态码: {response.status}"
|
||||||
except Exception as e:
|
|
||||||
print(f"解密失败: {e}")
|
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:
|
try:
|
||||||
# 尝试找到 JSON 开始的位置
|
decrypted_data = self._decrypt(encrypted, crypt_key)
|
||||||
for i in range(len(decrypted)):
|
result = json.loads(decrypted_data.decode("utf-8"))
|
||||||
try:
|
except Exception as e:
|
||||||
test_data = decrypted[i:]
|
return None, f"cookie解密失败: {str(e)}"
|
||||||
test_str = test_data.decode('utf-8', errors='ignore')
|
|
||||||
if test_str.strip().startswith('{'):
|
if not result:
|
||||||
print(f"找到可能的 JSON 开始位置: {i}")
|
return None, "cookie解密为空"
|
||||||
result = json.loads(test_str)
|
|
||||||
print("JSON 解析成功(直接截取)")
|
return result, ""
|
||||||
return result
|
|
||||||
except:
|
except urllib.error.HTTPError as e:
|
||||||
continue
|
return None, f"HTTP错误: {e.code} {e.reason}"
|
||||||
except:
|
except urllib.error.URLError as e:
|
||||||
pass
|
return None, f"网络连接失败: {e.reason}"
|
||||||
raise
|
except Exception as e:
|
||||||
|
return None, f"下载失败: {str(e)}"
|
||||||
|
|
||||||
def get_cookies(self) -> Dict[str, List[Dict]]:
|
def get_cookies(self) -> Dict[str, List[Dict]]:
|
||||||
"""
|
"""
|
||||||
@@ -107,49 +112,34 @@ class CookieCloud:
|
|||||||
按域名分组的 cookie 数据字典
|
按域名分组的 cookie 数据字典
|
||||||
格式: {"domain.com": [{"name": "cookie1", "value": "val1", ...}, ...], ...}
|
格式: {"domain.com": [{"name": "cookie1", "value": "val1", ...}, ...], ...}
|
||||||
"""
|
"""
|
||||||
url = f"{self.api_url}/get/{self.uuid}"
|
data, error = self._download_data()
|
||||||
|
|
||||||
try:
|
if error:
|
||||||
response = requests.get(url, timeout=30)
|
logger.error(error)
|
||||||
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}")
|
|
||||||
return {}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"处理 Cookie Cloud 数据失败: {e}")
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
# 处理数据结构
|
||||||
|
cookie_data = {}
|
||||||
|
|
||||||
|
# 兼容直接按域名分组的格式
|
||||||
|
if isinstance(data, dict) and not data.get('cookie_data'):
|
||||||
|
cookie_data = data
|
||||||
|
# 兼容包含 cookie_data 的格式
|
||||||
|
elif isinstance(data, dict) and data.get('cookie_data'):
|
||||||
|
cookie_data = data.get('cookie_data', {})
|
||||||
|
|
||||||
|
# 处理 sameSite 字段
|
||||||
|
processed_cookies = {}
|
||||||
|
for domain, cookies in cookie_data.items():
|
||||||
|
if not cookies:
|
||||||
|
continue
|
||||||
|
processed_cookies[domain] = []
|
||||||
|
for cookie in cookies:
|
||||||
|
if cookie.get('sameSite') == 'unspecified':
|
||||||
|
cookie['sameSite'] = 'Lax'
|
||||||
|
processed_cookies[domain].append(cookie)
|
||||||
|
|
||||||
|
return processed_cookies
|
||||||
|
|
||||||
def get_cookies_for_domain(self, domain: str) -> List[Dict]:
|
def get_cookies_for_domain(self, domain: str) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
@@ -163,16 +153,16 @@ class CookieCloud:
|
|||||||
"""
|
"""
|
||||||
all_cookies = self.get_cookies()
|
all_cookies = self.get_cookies()
|
||||||
|
|
||||||
# 查找匹配的域名
|
matched_cookies = []
|
||||||
for cookie_domain, cookies in all_cookies.items():
|
for cookie_domain, cookies in all_cookies.items():
|
||||||
if domain in cookie_domain or cookie_domain in domain:
|
clean_domain = cookie_domain.lstrip('.')
|
||||||
return cookies
|
if domain in clean_domain or clean_domain in domain:
|
||||||
|
matched_cookies.extend(cookies)
|
||||||
|
|
||||||
return []
|
return matched_cookies
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# 测试代码
|
|
||||||
cc = CookieCloud(
|
cc = CookieCloud(
|
||||||
api_url="https://movie-pilot.org/cookiecloud",
|
api_url="https://movie-pilot.org/cookiecloud",
|
||||||
uuid="hu6n2vcUmzpu7mqUN2rVCg",
|
uuid="hu6n2vcUmzpu7mqUN2rVCg",
|
||||||
@@ -180,4 +170,7 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
|
|
||||||
cookies = cc.get_cookies()
|
cookies = cc.get_cookies()
|
||||||
logger.info(json.dumps(cookies, indent=2, ensure_ascii=False))
|
logger.info(f"获取到 {len(cookies)} 个域名的 cookies")
|
||||||
|
|
||||||
|
lmkbi_cookies = cc.get_cookies_for_domain('lmkbi.95155.com')
|
||||||
|
logger.info(f"获取到 {len(lmkbi_cookies)} 个 lmkbi.95155.com 的 cookies")
|
||||||
|
|||||||
+106
-145
@@ -1,7 +1,5 @@
|
|||||||
"""Cookie 监控主程序"""
|
"""Cookie 监控主程序"""
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from cookie_cloud import CookieCloud
|
from cookie_cloud import CookieCloud
|
||||||
from notifier import IYUUNotifier, FailureTracker
|
from notifier import IYUUNotifier, FailureTracker
|
||||||
@@ -15,237 +13,200 @@ class CookieMonitor:
|
|||||||
"""Cookie 监控器"""
|
"""Cookie 监控器"""
|
||||||
|
|
||||||
def __init__(self, config_file: str = "config.json"):
|
def __init__(self, config_file: str = "config.json"):
|
||||||
"""
|
|
||||||
初始化 Cookie 监控器
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_file: 配置文件路径
|
|
||||||
"""
|
|
||||||
self.config_file = config_file
|
self.config_file = config_file
|
||||||
self.config = self._load_config()
|
self.config = self._load_config()
|
||||||
self.cookie_manager = CookieManager("cookies.json")
|
self.cookie_manager = CookieManager("cookies.json")
|
||||||
self.failure_tracker = FailureTracker("state.json")
|
self.failure_tracker = FailureTracker("state.json")
|
||||||
|
|
||||||
def _load_config(self) -> dict:
|
def _load_config(self) -> dict:
|
||||||
"""加载配置文件"""
|
|
||||||
try:
|
try:
|
||||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"加载配置文件失败: {e}")
|
logger.error(f"加载配置文件失败: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _log(self, message: str):
|
|
||||||
"""记录日志"""
|
|
||||||
logger.info(message)
|
|
||||||
|
|
||||||
def _get_domain_from_url(self, url: str) -> str:
|
def _get_domain_from_url(self, url: str) -> str:
|
||||||
"""从 URL 中提取域名"""
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
parsed = urlparse(url)
|
return urlparse(url).netloc
|
||||||
return parsed.netloc
|
|
||||||
|
|
||||||
def process_user(self, user_config: dict):
|
def process_user(self, user_config: dict):
|
||||||
"""
|
"""
|
||||||
处理单个用户的所有网站
|
处理单个用户:一次性导入所有cookies,然后验证各网站登录状态
|
||||||
|
|
||||||
Args:
|
|
||||||
user_config: 用户配置
|
|
||||||
"""
|
"""
|
||||||
user_name = user_config.get('name', '未知用户')
|
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', {})
|
cookie_cloud_config = user_config.get('cookie_cloud', {})
|
||||||
notification_config = user_config.get('notification', {})
|
notification_config = user_config.get('notification', {})
|
||||||
browser_config = user_config.get('browser', {})
|
browser_config = user_config.get('browser', {})
|
||||||
websites = user_config.get('websites', [])
|
websites = user_config.get('websites', [])
|
||||||
|
|
||||||
# 初始化爱语飞飞通知
|
if not websites:
|
||||||
|
logger.info(f"用户 {user_name} 没有配置网站,跳过")
|
||||||
|
return
|
||||||
|
|
||||||
iyuu_token = notification_config.get('iyuu_token', '')
|
iyuu_token = notification_config.get('iyuu_token', '')
|
||||||
max_fail_count = notification_config.get('max_fail_count', 3)
|
max_fail_count = notification_config.get('max_fail_count', 3)
|
||||||
notifier = IYUUNotifier(iyuu_token) if iyuu_token else None
|
notifier = IYUUNotifier(iyuu_token) if iyuu_token else None
|
||||||
|
|
||||||
# 初始化 Cookie Cloud
|
|
||||||
cookie_cloud = CookieCloud(
|
cookie_cloud = CookieCloud(
|
||||||
api_url=cookie_cloud_config.get('api_url', ''),
|
api_url=cookie_cloud_config.get('api_url', ''),
|
||||||
uuid=cookie_cloud_config.get('uuid', ''),
|
uuid=cookie_cloud_config.get('uuid', ''),
|
||||||
password=cookie_cloud_config.get('password', '')
|
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 = BrowserLogin(
|
||||||
browser_type=browser_config.get('type', 'edge'),
|
browser_type=browser_config.get('type', 'edge'),
|
||||||
headless=browser_config.get('headless', False)
|
headless=browser_config.get('headless', False)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
try:
|
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:
|
for website in websites:
|
||||||
self.process_website(
|
result = self.process_website(
|
||||||
user_name=user_name,
|
user_name=user_name,
|
||||||
website_config=website,
|
website_config=website,
|
||||||
cookie_cloud=cookie_cloud,
|
|
||||||
notifier=notifier,
|
|
||||||
max_fail_count=max_fail_count,
|
|
||||||
browser=browser
|
browser=browser
|
||||||
)
|
)
|
||||||
finally:
|
results.append(result)
|
||||||
# 关闭浏览器
|
|
||||||
browser.close()
|
|
||||||
|
|
||||||
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(
|
def process_website(
|
||||||
self,
|
self,
|
||||||
user_name: str,
|
user_name: str,
|
||||||
website_config: dict,
|
website_config: dict,
|
||||||
cookie_cloud: CookieCloud,
|
|
||||||
notifier: IYUUNotifier,
|
|
||||||
max_fail_count: int,
|
|
||||||
browser: BrowserLogin
|
browser: BrowserLogin
|
||||||
):
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
处理单个网站
|
验证网站登录状态(cookies已预先导入)
|
||||||
|
|
||||||
Args:
|
Returns:
|
||||||
user_name: 用户名
|
dict: {'name': 网站名, 'url': URL, 'success': 是否成功, 'error': 错误信息}
|
||||||
website_config: 网站配置
|
|
||||||
cookie_cloud: Cookie Cloud 客户端
|
|
||||||
notifier: 爱语飞飞通知客户端
|
|
||||||
max_fail_count: 最大失败次数
|
|
||||||
browser: 浏览器客户端
|
|
||||||
"""
|
"""
|
||||||
website_name = website_config.get('name', '未知网站')
|
website_name = website_config.get('name', '未知网站')
|
||||||
url = website_config.get('url', '')
|
url = website_config.get('url', '')
|
||||||
check_selector = website_config.get('login_check_selector', '')
|
check_selector = website_config.get('login_check_selector', '')
|
||||||
success_text = website_config.get('success_text', '')
|
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}")
|
||||||
|
|
||||||
# 获取域名
|
result = {
|
||||||
domain = self._get_domain_from_url(url)
|
'name': website_name,
|
||||||
|
'url': url,
|
||||||
|
'success': False,
|
||||||
|
'error': ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
result['error'] = 'URL 未配置'
|
||||||
|
logger.error(f" 失败: {result['error']}")
|
||||||
|
return result
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 第一步:使用本地 cookie 登录
|
login_success = browser.verify_login(
|
||||||
self._log(f"尝试使用本地 cookie 登录: {website_name}")
|
url=url,
|
||||||
local_cookies = self.cookie_manager.get_cookies(user_name, website_name)
|
check_selector=check_selector,
|
||||||
|
success_text=success_text
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if should_stop:
|
if login_success:
|
||||||
self._log(f"已达到最大失败次数,已发送通知并重置计数: {website_name}")
|
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:
|
else:
|
||||||
fail_count = self.failure_tracker.get_fail_count(user_name, website_name)
|
result['error'] = '登录状态验证失败(cookies可能已过期或选择器配置错误)'
|
||||||
self._log(f"当前失败次数: {fail_count}/{max_fail_count}")
|
logger.warning(f" ✗ {result['error']}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log(f"处理网站 {website_name} 时发生异常: {e}")
|
result['error'] = f'处理异常: {str(e)}'
|
||||||
error_msg = f"{website_name} 处理异常: {str(e)}"
|
logger.error(f" 异常: {e}")
|
||||||
|
|
||||||
should_stop = self.failure_tracker.check_and_notify(
|
return result
|
||||||
user_name=user_name,
|
|
||||||
website_name=website_name,
|
def _print_summary(self, user_name: str, results: list):
|
||||||
max_fail_count=max_fail_count,
|
"""打印处理结果汇总"""
|
||||||
notifier=notifier,
|
success_count = sum(1 for r in results if r['success'])
|
||||||
error_msg=error_msg
|
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):
|
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', [])
|
users = self.config.get('users', [])
|
||||||
|
|
||||||
if not users:
|
if not users:
|
||||||
self._log("未找到用户配置")
|
logger.error("未找到用户配置")
|
||||||
return
|
return
|
||||||
|
|
||||||
success_count = 0
|
|
||||||
fail_count = 0
|
|
||||||
|
|
||||||
for user_config in users:
|
for user_config in users:
|
||||||
try:
|
try:
|
||||||
self.process_user(user_config)
|
self.process_user(user_config)
|
||||||
success_count += 1
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log(f"处理用户失败: {e}")
|
logger.error(f"处理用户失败: {e}")
|
||||||
fail_count += 1
|
|
||||||
|
|
||||||
self._log(f"===== Cookie 监控结束 =====")
|
logger.info("\n" + "="*50)
|
||||||
self._log(f"成功处理: {success_count} 个用户,失败: {fail_count} 个用户")
|
logger.info("Cookie 监控结束")
|
||||||
|
logger.info("="*50)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""主函数"""
|
|
||||||
# 配置日志
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
monitor = CookieMonitor()
|
monitor = CookieMonitor()
|
||||||
monitor.run()
|
monitor.run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user