""" Playwright Manager - Hauptklasse für die Browser-Steuerung mit Anti-Bot-Erkennung """ import os import json import logging import random import time from pathlib import Path from typing import Dict, Optional, List, Any, Tuple from playwright.sync_api import sync_playwright, Browser, Page, BrowserContext, ElementHandle from domain.value_objects.browser_protection_style import BrowserProtectionStyle from infrastructure.services.browser_protection_service import BrowserProtectionService # Konfiguriere Logger logger = logging.getLogger("playwright_manager") class PlaywrightManager: """ Verwaltet Browser-Sitzungen mit Playwright, einschließlich Stealth-Modus und Proxy-Einstellungen. """ # Klassen-Variable: Zählt aktive Browser-Instanzen (Feature 5: Browser-Instanz Schutz) _active_count = 0 def __init__(self, headless: bool = False, proxy: Optional[Dict[str, str]] = None, browser_type: str = "chromium", browser_channel: Optional[str] = None, user_agent: Optional[str] = None, screenshots_dir: str = "screenshots", slowmo: int = 0, window_position: Optional[Tuple[int, int]] = None, enable_autoplay_flag: bool = True, auto_click_video: bool = True, auto_mute_on_play: bool = True, enable_video_enhancements: bool = False): """ Initialisiert den PlaywrightManager. Args: headless: Ob der Browser im Headless-Modus ausgeführt werden soll proxy: Proxy-Konfiguration (z.B. {'server': 'http://myproxy.com:3128', 'username': 'user', 'password': 'pass'}) browser_type: Welcher Browser-Typ verwendet werden soll ("chromium", "firefox", oder "webkit") user_agent: Benutzerdefinierter User-Agent screenshots_dir: Verzeichnis für Screenshots slowmo: Verzögerung zwischen Aktionen in Millisekunden (nützlich für Debugging) window_position: Optionale Fensterposition als Tupel (x, y) """ self.headless = headless self.proxy = proxy self.browser_type = browser_type self.browser_channel = browser_channel self.user_agent = user_agent self.screenshots_dir = screenshots_dir self.slowmo = slowmo self.window_position = window_position self.enable_autoplay_flag = enable_autoplay_flag self.auto_click_video = auto_click_video self.auto_mute_on_play = auto_mute_on_play self.enable_video_enhancements = enable_video_enhancements # Stelle sicher, dass das Screenshots-Verzeichnis existiert os.makedirs(self.screenshots_dir, exist_ok=True) # Playwright-Instanzen self.playwright = None self.browser = None self.context = None self.page = None # Zähler für Wiederhholungsversuche self.retry_counter = {} # Lade Browser-/Stealth-Konfigurationen self.browser_config = self._load_browser_config() self.stealth_config = self._load_stealth_config() # Browser Protection Service self.protection_service = BrowserProtectionService() self.protection_applied = False self.protection_style = None def _load_stealth_config(self) -> Dict[str, Any]: """Lädt die Stealth-Konfigurationen aus der Datei oder verwendet Standardwerte.""" try: config_dir = Path(__file__).parent.parent / "config" stealth_config_path = config_dir / "stealth_config.json" if stealth_config_path.exists(): with open(stealth_config_path, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: logger.warning(f"Konnte Stealth-Konfiguration nicht laden: {e}") # Verwende Standardwerte, wenn das Laden fehlschlägt return { "vendor": "Google Inc.", "platform": "Win32", "webdriver": False, "accept_language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", "timezone_id": "Europe/Berlin", "fingerprint_noise": True, "device_scale_factor": 1.0, } def _load_browser_config(self) -> Dict[str, Any]: """Lädt Browser-bezogene Konfigurationen (Channel, Autoplay-Flags, Video-Helper).""" try: config_dir = Path(__file__).parent.parent / "config" browser_config_path = config_dir / "browser_config.json" if browser_config_path.exists() and browser_config_path.stat().st_size > 0: with open(browser_config_path, 'r', encoding='utf-8') as f: data = json.load(f) return data if isinstance(data, dict) else {} except Exception as e: logger.warning(f"Konnte Browser-Konfiguration nicht laden: {e}") # Defaults: Chrome/Edge Channel und Autoplay erlauben return { "channel": "chrome", # Alternativ: "msedge" "enable_autoplay_flag": True, "auto_click_video": True, "auto_mute_on_play": True, "enable_video_enhancements": False } def start(self) -> Page: """ Startet die Playwright-Sitzung und gibt die Browser-Seite zurück. Returns: Page: Die Browser-Seite Raises: RuntimeError: Wenn bereits eine Browser-Instanz aktiv ist """ if self.page is not None: return self.page # Feature 5: Browser-Instanz Schutz - Nur eine Instanz gleichzeitig if PlaywrightManager._active_count >= 1: raise RuntimeError( "Browser bereits aktiv. Nur eine Browser-Instanz gleichzeitig erlaubt. " "Beenden Sie den aktuellen Prozess." ) try: self.playwright = sync_playwright().start() # Wähle den Browser-Typ if self.browser_type == "firefox": browser_instance = self.playwright.firefox elif self.browser_type == "webkit": browser_instance = self.playwright.webkit else: browser_instance = self.playwright.chromium # Browser-Startoptionen browser_args = [] if self.browser_type == "chromium": # Chrome-spezifische Argumente für Anti-Bot-Erkennung browser_args.extend([ '--disable-blink-features=AutomationControlled', '--disable-features=IsolateOrigins,site-per-process', '--disable-site-isolation-trials', ]) # Autoplay-Blockade aufheben (für Video-Tests/Automation) # Aktiviert nur, wenn konfiguriert if self.enable_autoplay_flag if self.enable_autoplay_flag is not None else self.browser_config.get("enable_autoplay_flag", True): browser_args.append('--autoplay-policy=no-user-gesture-required') # Browser-Launch-Optionen # Speichere Args für mögliche Erweiterungen (z.B. Gmail Fresh Browser) self.browser_args = browser_args launch_options = { "headless": self.headless, "args": browser_args, "slow_mo": self.slowmo } # Fensterposition setzen wenn angegeben if self.window_position and not self.headless: x, y = self.window_position browser_args.extend([ f'--window-position={x},{y}' ]) # Für Chromium optional einen Channel (chrome/msedge) nutzen # Fallback: Ohne Channel starten, wenn der Channel nicht verfügbar ist if self.browser_type == "chromium": # Bevorzugung: expliziter Parameter > Konfigurationswert channel = self.browser_channel if self.browser_channel else self.browser_config.get("channel") tried_channel = False if channel: try: logger.info(f"Starte Chromium mit Channel '{channel}' (für H.264/AAC Unterstützung)") self.browser = browser_instance.launch(channel=channel, **launch_options) tried_channel = True except Exception as e: logger.warning(f"Start mit Channel '{channel}' fehlgeschlagen: {e}.") # Zweiter Versuch mit alternativem Channel (msedge/chrome) alt = 'msedge' if channel.startswith('chrome') else 'chrome' try: logger.info(f"Versuche alternativen Channel '{alt}'") self.browser = browser_instance.launch(channel=alt, **launch_options) tried_channel = True except Exception as e2: logger.warning(f"Auch alternativer Channel '{alt}' fehlgeschlagen: {e2}. Fallback auf gebundleten Chromium.") # Fallback ohne Channel unten if not tried_channel or self.browser is None: self.browser = browser_instance.launch(**launch_options) else: # Andere Browser-Typen normal starten self.browser = browser_instance.launch(**launch_options) # Kontext-Optionen für Stealth-Modus context_options = { "viewport": {"width": 1920, "height": 1080}, "device_scale_factor": self.stealth_config.get("device_scale_factor", 1.0), "locale": "de-DE", "timezone_id": self.stealth_config.get("timezone_id", "Europe/Berlin"), "accept_downloads": True, } # User-Agent setzen if self.user_agent: context_options["user_agent"] = self.user_agent # Proxy-Einstellungen, falls vorhanden if self.proxy: context_options["proxy"] = self.proxy # Browserkontext erstellen self.context = self.browser.new_context(**context_options) # Optional: Video Stealth/Kompatibilitäts-Verbesserungen anwenden try: enable_video_enh = self.enable_video_enhancements if self.enable_video_enhancements is not None else self.browser_config.get("enable_video_enhancements", False) if enable_video_enh: from browser.video_stealth_enhancement import VideoStealthEnhancement VideoStealthEnhancement(self.context).apply_video_stealth() logger.info("Video-Stealth-Verbesserungen aktiviert") except Exception as e: logger.warning(f"Konnte Video-Stealth-Verbesserungen nicht anwenden: {e}") # JavaScript-Fingerprinting-Schutz self._apply_stealth_scripts() # Neue Seite erstellen self.page = self.context.new_page() # Event-Listener für Konsolen-Logs self.page.on("console", lambda msg: logger.debug(f"BROWSER CONSOLE: {msg.text}")) # Feature 5: Browser-Instanz Counter erhöhen PlaywrightManager._active_count += 1 logger.info(f"Browser gestartet (aktive Instanzen: {PlaywrightManager._active_count})") return self.page except Exception as e: logger.error(f"Fehler beim Starten des Browsers: {e}") self.close() raise def _apply_stealth_scripts(self): """Wendet JavaScript-Skripte an, um Browser-Fingerprinting zu umgehen.""" # Diese Skripte überschreiben Eigenschaften, die für Bot-Erkennung verwendet werden scripts = [ # WebDriver-Eigenschaft überschreiben """ () => { Object.defineProperty(navigator, 'webdriver', { get: () => false, }); } """, # Navigator-Eigenschaften überschreiben f""" () => {{ const newProto = navigator.__proto__; delete newProto.webdriver; navigator.__proto__ = newProto; Object.defineProperty(navigator, 'platform', {{ get: () => '{self.stealth_config.get("platform", "Win32")}' }}); Object.defineProperty(navigator, 'languages', {{ get: () => ['de-DE', 'de', 'en-US', 'en'] }}); Object.defineProperty(navigator, 'vendor', {{ get: () => '{self.stealth_config.get("vendor", "Google Inc.")}' }}); }} """, # Chrome-Objekte hinzufügen, die in normalen Browsern vorhanden sind """ () => { // Fügt chrome.runtime hinzu, falls nicht vorhanden if (!window.chrome) { window.chrome = {}; } if (!window.chrome.runtime) { window.chrome.runtime = {}; window.chrome.runtime.sendMessage = function() {}; } } """, # Plugin-Fingerprinting """ () => { const originalQuery = window.navigator.permissions.query; window.navigator.permissions.query = (parameters) => ( parameters.name === 'notifications' ? Promise.resolve({ state: Notification.permission }) : originalQuery(parameters) ); } """ ] # Wenn Fingerprint-Noise aktiviert ist, füge zufällige Variationen hinzu if self.stealth_config.get("fingerprint_noise", True): scripts.append(""" () => { // Canvas-Fingerprinting leicht verändern const originalToDataURL = HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.toDataURL = function(type) { const result = originalToDataURL.apply(this, arguments); if (this.width > 16 && this.height > 16) { // Kleines Rauschen in Pixels einfügen const context = this.getContext('2d'); const imageData = context.getImageData(0, 0, 2, 2); const pixelArray = imageData.data; // Ändere einen zufälligen Pixel leicht const randomPixel = Math.floor(Math.random() * pixelArray.length / 4) * 4; pixelArray[randomPixel] = (pixelArray[randomPixel] + Math.floor(Math.random() * 10)) % 256; context.putImageData(imageData, 0, 0); } return result; }; } """) # Skripte auf den Browser-Kontext anwenden for script in scripts: self.context.add_init_script(script) def navigate_to(self, url: str, wait_until: str = "networkidle", timeout: int = 30000) -> bool: """ Navigiert zu einer bestimmten URL und wartet, bis die Seite geladen ist. Args: url: Die Ziel-URL wait_until: Wann die Navigation als abgeschlossen gilt ("load", "domcontentloaded", "networkidle") timeout: Timeout in Millisekunden Returns: bool: True bei erfolgreicher Navigation, False sonst """ if self.page is None: self.start() try: logger.info(f"Navigiere zu: {url}") self.page.goto(url, wait_until=wait_until, timeout=timeout) # Nach erfolgreicher Navigation: Versuche Videos zu starten (optional) try: enable_auto_kick = self.enable_autoplay_flag if self.enable_autoplay_flag is not None else self.browser_config.get("enable_autoplay_flag", True) if enable_auto_kick: self._kickstart_video_playback() except Exception: pass return True except Exception as e: logger.error(f"Fehler bei der Navigation zu {url}: {e}") self.take_screenshot(f"navigation_error_{int(time.time())}") return False def _kickstart_video_playback(self) -> None: """ Versucht, die Wiedergabe von