This commit is contained in:
2026-03-03 08:53:50 +08:00
parent f8a14a3e2e
commit 0fbbbce2ee
4 changed files with 390 additions and 274 deletions
+111 -118
View File
@@ -1,11 +1,11 @@
"""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 applogger import get_logger
logger = get_logger(__name__)
@@ -27,77 +27,82 @@ class CookieCloud:
self.uuid = uuid
self.password = password
def decrypt(self, encrypted_data: str) -> Dict:
"""
解密 Cookie Cloud 数据
def _get_crypt_key(self) -> bytes:
"""生成加密密钥"""
combined_string = f"{self.uuid}-{self.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_data(self) -> Tuple[Optional[Dict], str]:
"""下载并解密所有数据"""
try:
# 移除 PKCS#7 填充
decrypted = unpad(decrypted, AES.block_size)
print(f"移除填充后长度: {len(decrypted)}")
print(f"移除填充后前 100 字节: {decrypted[:100]}")
url = f"{self.api_url}/get/{self.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=30)
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:
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,49 +112,34 @@ class CookieCloud:
按域名分组的 cookie 数据字典
格式: {"domain.com": [{"name": "cookie1", "value": "val1", ...}, ...], ...}
"""
url = f"{self.api_url}/get/{self.uuid}"
data, error = self._download_data()
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}")
if error:
logger.error(error)
return {}
# 处理数据结构
cookie_data = {}
# 兼容直接按域名分组的格式
if isinstance(data, dict) and not data.get('cookie_data'):
cookie_data = data
# 兼容包含 cookie_data 的格式
elif isinstance(data, dict) and data.get('cookie_data'):
cookie_data = data.get('cookie_data', {})
# 处理 sameSite 字段
processed_cookies = {}
for domain, cookies in cookie_data.items():
if not cookies:
continue
processed_cookies[domain] = []
for cookie in cookies:
if cookie.get('sameSite') == 'unspecified':
cookie['sameSite'] = 'Lax'
processed_cookies[domain].append(cookie)
return processed_cookies
def get_cookies_for_domain(self, domain: str) -> List[Dict]:
"""
@@ -163,16 +153,16 @@ 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 []
return matched_cookies
if __name__ == "__main__":
# 测试代码
cc = CookieCloud(
api_url="https://movie-pilot.org/cookiecloud",
uuid="hu6n2vcUmzpu7mqUN2rVCg",
@@ -180,4 +170,7 @@ if __name__ == "__main__":
)
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")