183 lines
6.4 KiB
Python
183 lines
6.4 KiB
Python
"""Cookie Cloud 模块 - 从 Cookie Cloud 服务器获取并解密 cookie 数据"""
|
|
import hashlib
|
|
import json
|
|
import base64
|
|
from typing import Dict, List, Optional
|
|
import requests
|
|
from Crypto.Cipher import AES
|
|
from Crypto.Util.Padding import unpad
|
|
from applogger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class CookieCloud:
|
|
"""Cookie Cloud 客户端"""
|
|
|
|
def __init__(self, api_url: str, uuid: str, password: str):
|
|
"""
|
|
初始化 Cookie Cloud 客户端
|
|
|
|
Args:
|
|
api_url: Cookie Cloud API 地址
|
|
uuid: 用户 UUID
|
|
password: 用户密码
|
|
"""
|
|
self.api_url = api_url.rstrip('/')
|
|
self.uuid = uuid
|
|
self.password = password
|
|
|
|
def decrypt(self, encrypted_data: str) -> Dict:
|
|
"""
|
|
解密 Cookie Cloud 数据
|
|
|
|
Args:
|
|
encrypted_data: 加密的数据字符串
|
|
|
|
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]}")
|
|
|
|
try:
|
|
# 移除 PKCS#7 填充
|
|
decrypted = unpad(decrypted, AES.block_size)
|
|
print(f"移除填充后长度: {len(decrypted)}")
|
|
print(f"移除填充后前 100 字节: {decrypted[:100]}")
|
|
|
|
# 尝试解析 JSON
|
|
result = json.loads(decrypted.decode('utf-8'))
|
|
print("JSON 解析成功")
|
|
return result
|
|
except Exception as e:
|
|
print(f"解密失败: {e}")
|
|
# 尝试直接截取可能的有效数据
|
|
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
|
|
|
|
def get_cookies(self) -> Dict[str, List[Dict]]:
|
|
"""
|
|
从 Cookie Cloud 服务器获取 cookie 数据
|
|
|
|
Returns:
|
|
按域名分组的 cookie 数据字典
|
|
格式: {"domain.com": [{"name": "cookie1", "value": "val1", ...}, ...], ...}
|
|
"""
|
|
url = f"{self.api_url}/get/{self.uuid}"
|
|
|
|
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}")
|
|
return {}
|
|
except Exception as e:
|
|
logger.error(f"处理 Cookie Cloud 数据失败: {e}")
|
|
return {}
|
|
|
|
def get_cookies_for_domain(self, domain: str) -> List[Dict]:
|
|
"""
|
|
获取指定域名的 cookie
|
|
|
|
Args:
|
|
domain: 域名
|
|
|
|
Returns:
|
|
cookie 列表
|
|
"""
|
|
all_cookies = self.get_cookies()
|
|
|
|
# 查找匹配的域名
|
|
for cookie_domain, cookies in all_cookies.items():
|
|
if domain in cookie_domain or cookie_domain in domain:
|
|
return cookies
|
|
|
|
return []
|
|
|
|
|
|
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)) |