update
This commit is contained in:
+111
-118
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user