395 lines
13 KiB
Python
395 lines
13 KiB
Python
"""
|
|
MoviePilot Cookie功能示例程序
|
|
从CookieCloud下载数据并注入浏览器访问网站
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
import hashlib
|
|
import base64
|
|
from typing import Dict, List, Optional, Tuple
|
|
from dataclasses import dataclass
|
|
import urllib.request
|
|
import urllib.error
|
|
from Crypto.Cipher import AES
|
|
|
|
|
|
@dataclass
|
|
class CookieCloudConfig:
|
|
"""CookieCloud配置"""
|
|
server: str
|
|
username: str
|
|
password: str
|
|
timeout: int = 30
|
|
|
|
|
|
class CookieCloudDownloader:
|
|
"""从CookieCloud服务器下载cookie数据"""
|
|
|
|
def __init__(self, config: CookieCloudConfig):
|
|
self.config = config
|
|
|
|
def _get_crypt_key(self) -> bytes:
|
|
combined_string = f"{self.config.username}-{self.config.password}"
|
|
return hashlib.md5(combined_string.encode('utf-8')).hexdigest()[:16].encode("utf-8")
|
|
|
|
def _bytes_to_key(self, data: bytes, salt: bytes, output: int = 48) -> bytes:
|
|
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]
|
|
|
|
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 _get_url_domain(self, domain: str) -> str:
|
|
if not domain:
|
|
return ""
|
|
domain = domain.lstrip('.')
|
|
if ":" in domain:
|
|
domain = domain.split(":")[0]
|
|
parts = domain.split(".")
|
|
if all(part.isdigit() for part in parts):
|
|
return domain
|
|
if len(parts) >= 2:
|
|
return ".".join(parts[-2:])
|
|
return domain
|
|
|
|
def download_all(self) -> Tuple[Optional[Dict], str]:
|
|
"""下载所有cookie和local storage数据"""
|
|
try:
|
|
url = f"{self.config.server}/get/{self.config.username}"
|
|
request = urllib.request.Request(
|
|
url,
|
|
headers={
|
|
'Content-Type': 'application/json',
|
|
'User-Agent': 'MoviePilot-Cookie-Client/1.0'
|
|
},
|
|
method='GET'
|
|
)
|
|
|
|
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:
|
|
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解密为空"
|
|
|
|
cookie_data = result.get("cookie_data", {})
|
|
local_storage_data = result.get("local_storage_data", {})
|
|
|
|
total_cookies = sum(len(v) for v in cookie_data.values())
|
|
total_ls = sum(len(v) for v in local_storage_data.values())
|
|
print(f" 下载完成: {len(cookie_data)} 个域名的Cookie({total_cookies}个), {len(local_storage_data)} 个域名的Local Storage({total_ls}个)")
|
|
|
|
return {"cookies": cookie_data, "local_storage": local_storage_data}, ""
|
|
|
|
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)}"
|
|
|
|
|
|
class BrowserController:
|
|
"""使用Playwright控制浏览器"""
|
|
|
|
def __init__(self, headless: bool = False, use_edge: bool = False):
|
|
self.headless = headless
|
|
self.use_edge = use_edge
|
|
self.browser = None
|
|
self.context = None
|
|
self.page = None
|
|
self.playwright = None
|
|
|
|
def start(self) -> bool:
|
|
try:
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
self.playwright = sync_playwright().start()
|
|
|
|
if self.use_edge:
|
|
self.browser = self.playwright.chromium.launch(
|
|
channel="msedge",
|
|
headless=self.headless,
|
|
args=['--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--no-sandbox']
|
|
)
|
|
else:
|
|
self.browser = self.playwright.chromium.launch(
|
|
headless=self.headless,
|
|
args=['--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--no-sandbox']
|
|
)
|
|
|
|
self.context = self.browser.new_context(
|
|
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
)
|
|
|
|
self.page = self.context.new_page()
|
|
|
|
print(f"✓ 浏览器启动成功 ({'Edge' if self.use_edge else 'Chromium'})")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"✗ 浏览器启动失败: {str(e)}")
|
|
return False
|
|
|
|
def inject_cookies(self, cookies: Dict):
|
|
"""注入cookie到浏览器"""
|
|
if not self.context or not cookies:
|
|
return
|
|
|
|
try:
|
|
playwright_cookies = []
|
|
for domain_key, cookie_list in cookies.items():
|
|
for cookie in cookie_list:
|
|
cookie_domain = cookie.get("domain", "").lstrip('.')
|
|
if not cookie_domain:
|
|
cookie_domain = domain_key.lstrip('.')
|
|
|
|
pw_cookie = {
|
|
"name": cookie.get("name", ""),
|
|
"value": cookie.get("value", ""),
|
|
"domain": cookie_domain,
|
|
"path": cookie.get("path", "/"),
|
|
}
|
|
if cookie.get("secure"):
|
|
pw_cookie["secure"] = True
|
|
if cookie.get("httpOnly"):
|
|
pw_cookie["httpOnly"] = True
|
|
if cookie.get("expirationDate"):
|
|
pw_cookie["expires"] = int(cookie.get("expirationDate"))
|
|
playwright_cookies.append(pw_cookie)
|
|
|
|
self.context.add_cookies(playwright_cookies)
|
|
print(f"✓ Cookie注入成功 ({len(playwright_cookies)} 个)")
|
|
except Exception as e:
|
|
print(f"✗ Cookie注入失败: {str(e)}")
|
|
|
|
def navigate(self, url: str, wait_time: int = 5, timeout: int = 60) -> bool:
|
|
if not self.page:
|
|
return False
|
|
|
|
try:
|
|
print(f"正在访问: {url}")
|
|
|
|
self.page.goto(url, timeout=timeout * 1000)
|
|
|
|
try:
|
|
self.page.wait_for_load_state("networkidle", timeout=timeout * 1000)
|
|
except Exception:
|
|
print(" networkidle超时,尝试load状态...")
|
|
try:
|
|
self.page.wait_for_load_state("load", timeout=30000)
|
|
except Exception:
|
|
print(" load超时,尝试domcontentloaded状态...")
|
|
self.page.wait_for_load_state("domcontentloaded", timeout=10000)
|
|
|
|
time.sleep(wait_time)
|
|
|
|
print(f"✓ 页面加载完成")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"✗ 页面访问失败: {str(e)}")
|
|
return False
|
|
|
|
def check_login_status(self, login_indicator: str) -> bool:
|
|
if not self.page:
|
|
return False
|
|
|
|
try:
|
|
html_content = self.page.content()
|
|
|
|
if login_indicator:
|
|
if login_indicator in html_content:
|
|
print(f"✓ 检测到登录指示器: {login_indicator}")
|
|
return True
|
|
else:
|
|
print(f"✗ 未检测到登录指示器: {login_indicator}")
|
|
return False
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
print(f"✗ 登录状态检查失败: {str(e)}")
|
|
return False
|
|
|
|
def take_screenshot(self, filepath: str):
|
|
if not self.page:
|
|
return
|
|
|
|
try:
|
|
self.page.screenshot(path=filepath)
|
|
print(f"✓ 截图已保存: {filepath}")
|
|
except Exception as e:
|
|
print(f"✗ 截图失败: {str(e)}")
|
|
|
|
def close(self):
|
|
try:
|
|
if self.browser:
|
|
self.browser.close()
|
|
|
|
if self.playwright:
|
|
self.playwright.stop()
|
|
|
|
print("✓ 浏览器已关闭")
|
|
|
|
except Exception as e:
|
|
print(f"✗ 关闭浏览器失败: {str(e)}")
|
|
|
|
|
|
class CookieDemo:
|
|
"""Cookie功能示例"""
|
|
|
|
def __init__(self, cookiecloud_config: CookieCloudConfig):
|
|
self.downloader = CookieCloudDownloader(cookiecloud_config)
|
|
self.browser = None
|
|
|
|
def run_sites(self, sites: list, headless: bool = False, use_edge: bool = False):
|
|
"""下载一次数据,依次访问多个网站"""
|
|
# 下载数据
|
|
print("=" * 60)
|
|
print("从CookieCloud下载数据")
|
|
print("=" * 60)
|
|
|
|
data, error = self.downloader.download_all()
|
|
|
|
if error:
|
|
print(f"✗ 数据下载失败: {error}")
|
|
return
|
|
|
|
# 启动浏览器
|
|
print(f"\n启动浏览器")
|
|
print("-" * 60)
|
|
|
|
self.browser = BrowserController(headless=headless, use_edge=use_edge)
|
|
|
|
if not self.browser.start():
|
|
return
|
|
|
|
# 注入所有cookie
|
|
if data and data.get("cookies"):
|
|
print(f"\n注入所有cookie到浏览器")
|
|
print("-" * 60)
|
|
self.browser.inject_cookies(data["cookies"])
|
|
|
|
# 依次访问每个网站
|
|
for idx, site in enumerate(sites, 1):
|
|
print(f"\n\n{'=' * 60}")
|
|
print(f"访问网站 {idx}/{len(sites)}: {site['name']}")
|
|
print(f"URL: {site['url']}")
|
|
print("=" * 60)
|
|
|
|
if not self.browser.navigate(site['url']):
|
|
print(f"✗ 页面访问失败")
|
|
continue
|
|
|
|
# 验证登录状态
|
|
from urllib.parse import urlparse
|
|
parsed = urlparse(site['url'])
|
|
target_domain = parsed.netloc
|
|
|
|
login_indicator = site.get('login_indicator')
|
|
is_logged_in = self.browser.check_login_status(login_indicator)
|
|
|
|
if is_logged_in:
|
|
print("✓ Cookie验证成功:已登录状态")
|
|
else:
|
|
print("⚠ Cookie验证失败:未检测到登录状态")
|
|
|
|
# 截图
|
|
screenshot_path = f"{target_domain.replace('.', '_').replace(':', '_')}_screenshot.png"
|
|
self.browser.take_screenshot(screenshot_path)
|
|
|
|
if idx < len(sites):
|
|
print("\n等待3秒后继续下一个网站...")
|
|
time.sleep(3)
|
|
|
|
# 关闭浏览器
|
|
print(f"\n关闭浏览器")
|
|
print("-" * 60)
|
|
self.browser.close()
|
|
|
|
print("\n" + "=" * 60)
|
|
print("所有网站访问完成")
|
|
print("=" * 60)
|
|
|
|
|
|
def main():
|
|
# 配置CookieCloud
|
|
config = CookieCloudConfig(
|
|
server="http://192.168.1.100:3000/cookiecloud",
|
|
username="qyZpTrgiP5mVwiRZwfBhjz",
|
|
password="iGn1B4FnWftp3oj4Ko3jxA",
|
|
timeout=30
|
|
)
|
|
|
|
# 创建示例程序
|
|
demo = CookieDemo(config)
|
|
|
|
# 测试网站列表
|
|
test_sites = [
|
|
{
|
|
"url": "https://lmkbi.95155.com/bi-system/#/login",
|
|
"name": "BI系统",
|
|
"login_indicator": "彭峰"
|
|
},
|
|
{
|
|
"url": "http://192.168.1.100:3000/#/subscribe/movie",
|
|
"name": "MoviePilot订阅",
|
|
"login_indicator": "搜索"
|
|
},
|
|
{
|
|
"url": "http://192.168.1.102:8418/bwadmin/QueryCarInfo2",
|
|
"name": "QueryCarInfo2",
|
|
"login_indicator": "工单管理"
|
|
}
|
|
]
|
|
|
|
# 下载一次数据,依次访问所有网站
|
|
demo.run_sites(
|
|
sites=test_sites,
|
|
headless=False,
|
|
use_edge=True
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|