Initial commit
Dieser Commit ist enthalten in:
0
browser/__init__.py
Normale Datei
0
browser/__init__.py
Normale Datei
251
browser/cookie_consent_handler.py
Normale Datei
251
browser/cookie_consent_handler.py
Normale Datei
@ -0,0 +1,251 @@
|
||||
"""
|
||||
Cookie Consent Handler für Browser-Sessions
|
||||
|
||||
Behandelt Cookie-Consent-Seiten bei der Session-Wiederherstellung
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from playwright.sync_api import Page
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CookieConsentHandler:
|
||||
"""Behandelt Cookie-Consent-Dialoge verschiedener Plattformen"""
|
||||
|
||||
@staticmethod
|
||||
def handle_instagram_consent(page: Page) -> bool:
|
||||
"""
|
||||
Behandelt Instagram's Cookie-Consent-Seite
|
||||
|
||||
Args:
|
||||
page: Playwright Page-Objekt
|
||||
|
||||
Returns:
|
||||
bool: True wenn Consent behandelt wurde, False sonst
|
||||
"""
|
||||
try:
|
||||
# Warte kurz auf Seitenladung
|
||||
page.wait_for_load_state('networkidle', timeout=5000)
|
||||
|
||||
# Prüfe ob wir auf der Cookie-Consent-Seite sind
|
||||
consent_indicators = [
|
||||
# Deutsche Texte
|
||||
"text=/.*cookies erlauben.*/i",
|
||||
"text=/.*optionale cookies ablehnen.*/i",
|
||||
"button:has-text('Optionale Cookies ablehnen')",
|
||||
"button:has-text('Nur erforderliche Cookies erlauben')",
|
||||
# Englische Texte
|
||||
"button:has-text('Decline optional cookies')",
|
||||
"button:has-text('Only allow essential cookies')",
|
||||
# Allgemeine Selektoren
|
||||
"[aria-label*='cookie']",
|
||||
"text=/.*verwendung von cookies.*/i"
|
||||
]
|
||||
|
||||
# Versuche "Optionale Cookies ablehnen" zu klicken (datenschutzfreundlich)
|
||||
decline_buttons = [
|
||||
"button:has-text('Optionale Cookies ablehnen')",
|
||||
"button:has-text('Nur erforderliche Cookies erlauben')",
|
||||
"button:has-text('Decline optional cookies')",
|
||||
"button:has-text('Only allow essential cookies')"
|
||||
]
|
||||
|
||||
for button_selector in decline_buttons:
|
||||
try:
|
||||
button = page.locator(button_selector).first
|
||||
if button.is_visible():
|
||||
logger.info(f"Found consent decline button: {button_selector}")
|
||||
|
||||
# Verwende robuste Click-Methoden für Cookie-Consent
|
||||
success = False
|
||||
try:
|
||||
# Strategie 1: Standard Click
|
||||
button.click(timeout=5000)
|
||||
success = True
|
||||
except Exception as click_error:
|
||||
logger.warning(f"Standard click fehlgeschlagen: {click_error}")
|
||||
|
||||
# Strategie 2: Force Click
|
||||
try:
|
||||
button.click(force=True, timeout=5000)
|
||||
success = True
|
||||
except Exception as force_error:
|
||||
logger.warning(f"Force click fehlgeschlagen: {force_error}")
|
||||
|
||||
# Strategie 3: JavaScript Click
|
||||
try:
|
||||
js_result = page.evaluate(f"""
|
||||
() => {{
|
||||
const button = document.querySelector('{button_selector}');
|
||||
if (button) {{
|
||||
button.click();
|
||||
return true;
|
||||
}}
|
||||
return false;
|
||||
}}
|
||||
""")
|
||||
if js_result:
|
||||
success = True
|
||||
logger.info("JavaScript click erfolgreich für Cookie-Consent")
|
||||
except Exception as js_error:
|
||||
logger.warning(f"JavaScript click fehlgeschlagen: {js_error}")
|
||||
|
||||
if success:
|
||||
logger.info("Clicked decline optional cookies button")
|
||||
|
||||
# Warte auf Navigation
|
||||
page.wait_for_load_state('networkidle', timeout=5000)
|
||||
|
||||
# Setze Consent im LocalStorage
|
||||
page.evaluate("""
|
||||
() => {
|
||||
// Instagram Consent Storage für "nur erforderliche Cookies"
|
||||
localStorage.setItem('ig_cb', '2'); // 2 = nur erforderliche Cookies
|
||||
localStorage.setItem('ig_consent_timestamp', Date.now().toString());
|
||||
|
||||
// Meta Consent
|
||||
localStorage.setItem('consent_status', 'essential_only');
|
||||
localStorage.setItem('cookie_consent', JSON.stringify({
|
||||
necessary: true,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
""")
|
||||
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Alle Click-Strategien für Cookie-Consent Button fehlgeschlagen: {button_selector}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.debug(f"Consent check failed for {button_selector}: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Prüfe ob Consent-Seite überhaupt angezeigt wird
|
||||
for indicator in consent_indicators:
|
||||
try:
|
||||
if page.locator(indicator).first.is_visible():
|
||||
logger.warning("Cookie consent page detected but couldn't find decline button")
|
||||
|
||||
# Als letzter Ausweg: Akzeptiere alle Cookies
|
||||
accept_buttons = [
|
||||
"button:has-text('Alle Cookies erlauben')",
|
||||
"button:has-text('Allow all cookies')",
|
||||
"button:has-text('Accept all')",
|
||||
# Spezifischer Instagram-Selektor basierend auf div-role
|
||||
"div[role='button']:has-text('Alle Cookies erlauben')",
|
||||
# Fallback mit Partial Text
|
||||
"[role='button']:has-text('Cookies erlauben')",
|
||||
# XPath als letzter Fallback
|
||||
"xpath=//div[@role='button' and contains(text(),'Alle Cookies erlauben')]"
|
||||
]
|
||||
|
||||
for accept_button in accept_buttons:
|
||||
try:
|
||||
button = page.locator(accept_button).first
|
||||
if button.is_visible():
|
||||
logger.info(f"Fallback: Accepting all cookies with {accept_button}")
|
||||
|
||||
# Verwende robuste Click-Methoden
|
||||
success = False
|
||||
try:
|
||||
# Strategie 1: Standard Click
|
||||
button.click(timeout=5000)
|
||||
success = True
|
||||
except Exception as click_error:
|
||||
logger.warning(f"Standard click fehlgeschlagen für Accept: {click_error}")
|
||||
|
||||
# Strategie 2: Force Click
|
||||
try:
|
||||
button.click(force=True, timeout=5000)
|
||||
success = True
|
||||
except Exception as force_error:
|
||||
logger.warning(f"Force click fehlgeschlagen für Accept: {force_error}")
|
||||
|
||||
# Strategie 3: JavaScript Click für div[role='button']
|
||||
try:
|
||||
# Spezielle Behandlung für div-basierte Buttons
|
||||
js_result = page.evaluate("""
|
||||
(selector) => {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
for (const elem of elements) {
|
||||
if (elem && elem.textContent && elem.textContent.includes('Cookies erlauben')) {
|
||||
elem.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Fallback: Suche nach role='button' mit Text
|
||||
const roleButtons = document.querySelectorAll('[role="button"]');
|
||||
for (const btn of roleButtons) {
|
||||
if (btn && btn.textContent && btn.textContent.includes('Cookies erlauben')) {
|
||||
btn.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
""", "[role='button']")
|
||||
|
||||
if js_result:
|
||||
success = True
|
||||
logger.info("JavaScript click erfolgreich für Cookie Accept Button")
|
||||
except Exception as js_error:
|
||||
logger.warning(f"JavaScript click fehlgeschlagen für Accept: {js_error}")
|
||||
|
||||
if success:
|
||||
page.wait_for_load_state('networkidle', timeout=5000)
|
||||
|
||||
# Setze Consent im LocalStorage für "alle Cookies"
|
||||
page.evaluate("""
|
||||
() => {
|
||||
// Instagram Consent Storage für "alle Cookies"
|
||||
localStorage.setItem('ig_cb', '1'); // 1 = alle Cookies akzeptiert
|
||||
localStorage.setItem('ig_consent_timestamp', Date.now().toString());
|
||||
|
||||
// Meta Consent
|
||||
localStorage.setItem('consent_status', 'all_accepted');
|
||||
localStorage.setItem('cookie_consent', JSON.stringify({
|
||||
necessary: true,
|
||||
analytics: true,
|
||||
marketing: true,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
""")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Accept-Button {accept_button}: {e}")
|
||||
continue
|
||||
|
||||
return False
|
||||
except:
|
||||
continue
|
||||
|
||||
logger.debug("No cookie consent page detected")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling cookie consent: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def check_and_handle_consent(page: Page, platform: str = "instagram") -> bool:
|
||||
"""
|
||||
Prüft und behandelt Cookie-Consent für die angegebene Plattform
|
||||
|
||||
Args:
|
||||
page: Playwright Page-Objekt
|
||||
platform: Plattform-Name (default: "instagram")
|
||||
|
||||
Returns:
|
||||
bool: True wenn Consent behandelt wurde, False sonst
|
||||
"""
|
||||
if platform.lower() == "instagram":
|
||||
return CookieConsentHandler.handle_instagram_consent(page)
|
||||
else:
|
||||
logger.warning(f"No consent handler implemented for platform: {platform}")
|
||||
return False
|
||||
1119
browser/fingerprint_protection.py
Normale Datei
1119
browser/fingerprint_protection.py
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
521
browser/instagram_video_bypass.py
Normale Datei
521
browser/instagram_video_bypass.py
Normale Datei
@ -0,0 +1,521 @@
|
||||
# Instagram Video Bypass - Emergency Deep Level Fixes
|
||||
"""
|
||||
Tiefgreifende Instagram Video Bypass Techniken
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
logger = logging.getLogger("instagram_video_bypass")
|
||||
|
||||
class InstagramVideoBypass:
|
||||
"""Deep-level Instagram video bypass techniques"""
|
||||
|
||||
def __init__(self, page: Any):
|
||||
self.page = page
|
||||
|
||||
def apply_emergency_bypass(self) -> None:
|
||||
"""Wendet Emergency Deep-Level Bypass an"""
|
||||
|
||||
# 1. Complete Automation Signature Removal
|
||||
automation_removal_script = """
|
||||
() => {
|
||||
// Remove ALL automation signatures
|
||||
|
||||
// 1. Navigator properties cleanup
|
||||
delete navigator.__webdriver_script_fn;
|
||||
delete navigator.__fxdriver_evaluate;
|
||||
delete navigator.__driver_unwrapped;
|
||||
delete navigator.__webdriver_unwrapped;
|
||||
delete navigator.__driver_evaluate;
|
||||
delete navigator.__selenium_unwrapped;
|
||||
delete navigator.__fxdriver_unwrapped;
|
||||
|
||||
// 2. Window properties cleanup
|
||||
delete window.navigator.webdriver;
|
||||
delete window.webdriver;
|
||||
delete window.chrome.webdriver;
|
||||
delete window.callPhantom;
|
||||
delete window._phantom;
|
||||
delete window.__nightmare;
|
||||
delete window._selenium;
|
||||
delete window.calledSelenium;
|
||||
delete window.$cdc_asdjflasutopfhvcZLmcfl_;
|
||||
delete window.$chrome_asyncScriptInfo;
|
||||
delete window.__webdriver_evaluate;
|
||||
delete window.__selenium_evaluate;
|
||||
delete window.__webdriver_script_function;
|
||||
delete window.__webdriver_script_func;
|
||||
delete window.__webdriver_script_fn;
|
||||
delete window.__fxdriver_evaluate;
|
||||
delete window.__driver_unwrapped;
|
||||
delete window.__webdriver_unwrapped;
|
||||
delete window.__driver_evaluate;
|
||||
delete window.__selenium_unwrapped;
|
||||
delete window.__fxdriver_unwrapped;
|
||||
|
||||
// 3. Document cleanup
|
||||
delete document.__webdriver_script_fn;
|
||||
delete document.__selenium_unwrapped;
|
||||
delete document.__webdriver_unwrapped;
|
||||
delete document.__driver_evaluate;
|
||||
delete document.__webdriver_evaluate;
|
||||
delete document.__fxdriver_evaluate;
|
||||
delete document.__fxdriver_unwrapped;
|
||||
delete document.__driver_unwrapped;
|
||||
|
||||
// 4. Chrome object enhancement
|
||||
if (!window.chrome) {
|
||||
window.chrome = {};
|
||||
}
|
||||
if (!window.chrome.runtime) {
|
||||
window.chrome.runtime = {
|
||||
onConnect: {addListener: function() {}},
|
||||
onMessage: {addListener: function() {}},
|
||||
connect: function() { return {postMessage: function() {}, onMessage: {addListener: function() {}}} }
|
||||
};
|
||||
}
|
||||
if (!window.chrome.app) {
|
||||
window.chrome.app = {
|
||||
isInstalled: false,
|
||||
InstallState: {DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed'},
|
||||
RunningState: {CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running'}
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Plugin array enhancement
|
||||
const fakePlugins = [
|
||||
{name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format'},
|
||||
{name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: 'Portable Document Format'},
|
||||
{name: 'Native Client', filename: 'internal-nacl-plugin', description: 'Native Client'}
|
||||
];
|
||||
|
||||
Object.defineProperty(navigator, 'plugins', {
|
||||
get: () => {
|
||||
const pluginArray = [...fakePlugins];
|
||||
pluginArray.length = fakePlugins.length;
|
||||
pluginArray.item = function(index) { return this[index] || null; };
|
||||
pluginArray.namedItem = function(name) { return this.find(p => p.name === name) || null; };
|
||||
pluginArray.refresh = function() {};
|
||||
return pluginArray;
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
# 2. Instagram-specific video API spoofing
|
||||
instagram_video_api_script = """
|
||||
() => {
|
||||
// Instagram Video API Deep Spoofing
|
||||
|
||||
// 1. MSE (Media Source Extensions) proper support
|
||||
if (window.MediaSource) {
|
||||
const originalIsTypeSupported = MediaSource.isTypeSupported;
|
||||
MediaSource.isTypeSupported = function(type) {
|
||||
const supportedTypes = [
|
||||
'video/mp4; codecs="avc1.42E01E"',
|
||||
'video/mp4; codecs="avc1.4D401F"',
|
||||
'video/mp4; codecs="avc1.640028"',
|
||||
'video/webm; codecs="vp8"',
|
||||
'video/webm; codecs="vp9"',
|
||||
'audio/mp4; codecs="mp4a.40.2"',
|
||||
'audio/webm; codecs="opus"'
|
||||
];
|
||||
|
||||
if (supportedTypes.includes(type)) {
|
||||
return true;
|
||||
}
|
||||
return originalIsTypeSupported.call(this, type);
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Encrypted Media Extensions deep spoofing
|
||||
if (navigator.requestMediaKeySystemAccess) {
|
||||
const originalRequestAccess = navigator.requestMediaKeySystemAccess;
|
||||
navigator.requestMediaKeySystemAccess = function(keySystem, supportedConfigurations) {
|
||||
if (keySystem === 'com.widevine.alpha') {
|
||||
return Promise.resolve({
|
||||
keySystem: 'com.widevine.alpha',
|
||||
getConfiguration: () => ({
|
||||
initDataTypes: ['cenc', 'keyids', 'webm'],
|
||||
audioCapabilities: [
|
||||
{contentType: 'audio/mp4; codecs="mp4a.40.2"', robustness: 'SW_SECURE_CRYPTO'},
|
||||
{contentType: 'audio/webm; codecs="opus"', robustness: 'SW_SECURE_CRYPTO'}
|
||||
],
|
||||
videoCapabilities: [
|
||||
{contentType: 'video/mp4; codecs="avc1.42E01E"', robustness: 'SW_SECURE_DECODE'},
|
||||
{contentType: 'video/mp4; codecs="avc1.4D401F"', robustness: 'SW_SECURE_DECODE'},
|
||||
{contentType: 'video/webm; codecs="vp9"', robustness: 'SW_SECURE_DECODE'}
|
||||
],
|
||||
distinctiveIdentifier: 'optional',
|
||||
persistentState: 'required',
|
||||
sessionTypes: ['temporary', 'persistent-license']
|
||||
}),
|
||||
createMediaKeys: () => Promise.resolve({
|
||||
createSession: (sessionType) => {
|
||||
const session = {
|
||||
sessionId: 'session_' + Math.random().toString(36).substr(2, 9),
|
||||
expiration: NaN,
|
||||
closed: Promise.resolve(),
|
||||
keyStatuses: new Map(),
|
||||
addEventListener: function() {},
|
||||
removeEventListener: function() {},
|
||||
generateRequest: function(initDataType, initData) {
|
||||
setTimeout(() => {
|
||||
if (this.onmessage) {
|
||||
this.onmessage({
|
||||
type: 'message',
|
||||
message: new ArrayBuffer(8)
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
return Promise.resolve();
|
||||
},
|
||||
load: function() { return Promise.resolve(false); },
|
||||
update: function(response) {
|
||||
setTimeout(() => {
|
||||
if (this.onkeystatuseschange) {
|
||||
this.onkeystatuseschange();
|
||||
}
|
||||
}, 50);
|
||||
return Promise.resolve();
|
||||
},
|
||||
close: function() { return Promise.resolve(); },
|
||||
remove: function() { return Promise.resolve(); }
|
||||
};
|
||||
|
||||
// Add event target methods
|
||||
session.dispatchEvent = function() {};
|
||||
|
||||
return session;
|
||||
},
|
||||
setServerCertificate: () => Promise.resolve(true)
|
||||
})
|
||||
});
|
||||
}
|
||||
return originalRequestAccess.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Hardware media key handling
|
||||
if (navigator.mediaSession) {
|
||||
navigator.mediaSession.setActionHandler = function() {};
|
||||
navigator.mediaSession.playbackState = 'playing';
|
||||
} else {
|
||||
navigator.mediaSession = {
|
||||
metadata: null,
|
||||
playbackState: 'playing',
|
||||
setActionHandler: function() {},
|
||||
setPositionState: function() {}
|
||||
};
|
||||
}
|
||||
|
||||
// 4. Picture-in-Picture API
|
||||
if (!document.pictureInPictureEnabled) {
|
||||
Object.defineProperty(document, 'pictureInPictureEnabled', {
|
||||
get: () => true,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Web Audio API enhancement for video
|
||||
if (window.AudioContext || window.webkitAudioContext) {
|
||||
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
||||
const originalAudioContext = AudioCtx;
|
||||
|
||||
window.AudioContext = function(...args) {
|
||||
const ctx = new originalAudioContext(...args);
|
||||
|
||||
// Override audio context properties for consistency
|
||||
Object.defineProperty(ctx, 'baseLatency', {
|
||||
get: () => 0.01,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(ctx, 'outputLatency', {
|
||||
get: () => 0.02,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
// Copy static methods
|
||||
Object.keys(originalAudioContext).forEach(key => {
|
||||
window.AudioContext[key] = originalAudioContext[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# 3. Network request interception for video
|
||||
network_interception_script = """
|
||||
() => {
|
||||
// Advanced network request interception for Instagram videos
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(input, init) {
|
||||
const url = typeof input === 'string' ? input : input.url;
|
||||
|
||||
// Instagram video CDN requests
|
||||
if (url.includes('instagram.com') || url.includes('fbcdn.net') || url.includes('cdninstagram.com')) {
|
||||
const enhancedInit = {
|
||||
...init,
|
||||
headers: {
|
||||
...init?.headers,
|
||||
'Accept': '*/*',
|
||||
'Accept-Encoding': 'identity;q=1, *;q=0',
|
||||
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||
'Cache-Control': 'no-cache',
|
||||
'DNT': '1',
|
||||
'Origin': 'https://www.instagram.com',
|
||||
'Pragma': 'no-cache',
|
||||
'Referer': 'https://www.instagram.com/',
|
||||
'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"Windows"',
|
||||
'Sec-Fetch-Dest': 'video',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
'User-Agent': navigator.userAgent,
|
||||
'X-Asbd-Id': '129477',
|
||||
'X-Fb-Lsd': document.querySelector('[name="fb_dtsg"]')?.value || '',
|
||||
'X-Instagram-Ajax': '1'
|
||||
}
|
||||
};
|
||||
|
||||
// Remove problematic headers that might indicate automation
|
||||
delete enhancedInit.headers['sec-ch-ua-arch'];
|
||||
delete enhancedInit.headers['sec-ch-ua-bitness'];
|
||||
delete enhancedInit.headers['sec-ch-ua-full-version'];
|
||||
delete enhancedInit.headers['sec-ch-ua-full-version-list'];
|
||||
delete enhancedInit.headers['sec-ch-ua-model'];
|
||||
delete enhancedInit.headers['sec-ch-ua-wow64'];
|
||||
|
||||
return originalFetch.call(this, input, enhancedInit);
|
||||
}
|
||||
|
||||
return originalFetch.apply(this, arguments);
|
||||
};
|
||||
|
||||
// XMLHttpRequest interception
|
||||
const originalXHROpen = XMLHttpRequest.prototype.open;
|
||||
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
|
||||
this._url = url;
|
||||
return originalXHROpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
const originalXHRSend = XMLHttpRequest.prototype.send;
|
||||
XMLHttpRequest.prototype.send = function(body) {
|
||||
if (this._url && (this._url.includes('instagram.com') || this._url.includes('fbcdn.net'))) {
|
||||
// Add video-specific headers
|
||||
this.setRequestHeader('Accept', '*/*');
|
||||
this.setRequestHeader('Accept-Language', 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7');
|
||||
this.setRequestHeader('Cache-Control', 'no-cache');
|
||||
this.setRequestHeader('Pragma', 'no-cache');
|
||||
this.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
}
|
||||
return originalXHRSend.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
"""
|
||||
|
||||
# 4. Timing and behavior normalization
|
||||
timing_script = """
|
||||
() => {
|
||||
// Normalize timing functions to avoid detection
|
||||
|
||||
// Performance timing spoofing
|
||||
if (window.performance && window.performance.timing) {
|
||||
const timing = performance.timing;
|
||||
const now = Date.now();
|
||||
|
||||
Object.defineProperty(performance.timing, 'navigationStart', {
|
||||
get: () => now - Math.floor(Math.random() * 1000) - 1000,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(performance.timing, 'loadEventEnd', {
|
||||
get: () => now - Math.floor(Math.random() * 500),
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
// Date/Time consistency
|
||||
const originalDate = Date;
|
||||
const startTime = originalDate.now();
|
||||
|
||||
Date.now = function() {
|
||||
return startTime + (originalDate.now() - startTime);
|
||||
};
|
||||
|
||||
// Remove timing inconsistencies that indicate automation
|
||||
const originalSetTimeout = window.setTimeout;
|
||||
window.setTimeout = function(fn, delay, ...args) {
|
||||
// Add slight randomization to timing
|
||||
const randomDelay = delay + Math.floor(Math.random() * 10) - 5;
|
||||
return originalSetTimeout.call(this, fn, Math.max(0, randomDelay), ...args);
|
||||
};
|
||||
}
|
||||
"""
|
||||
|
||||
# Apply all scripts in sequence
|
||||
scripts = [
|
||||
automation_removal_script,
|
||||
instagram_video_api_script,
|
||||
network_interception_script,
|
||||
timing_script
|
||||
]
|
||||
|
||||
for i, script in enumerate(scripts):
|
||||
try:
|
||||
self.page.add_init_script(script)
|
||||
logger.info(f"Applied emergency bypass script {i+1}/4")
|
||||
time.sleep(0.1) # Small delay between scripts
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply emergency bypass script {i+1}: {e}")
|
||||
|
||||
logger.info("Emergency Instagram video bypass applied")
|
||||
|
||||
def inject_video_session_data(self) -> None:
|
||||
"""Injiziert realistische Video-Session-Daten"""
|
||||
|
||||
session_script = """
|
||||
() => {
|
||||
// Inject realistic video session data
|
||||
|
||||
// 1. Video viewing history
|
||||
localStorage.setItem('instagram_video_history', JSON.stringify({
|
||||
last_viewed: Date.now() - Math.floor(Math.random() * 86400000),
|
||||
view_count: Math.floor(Math.random() * 50) + 10,
|
||||
preferences: {
|
||||
autoplay: true,
|
||||
quality: 'auto',
|
||||
captions: false
|
||||
}
|
||||
}));
|
||||
|
||||
// 2. Media session state
|
||||
localStorage.setItem('media_session_state', JSON.stringify({
|
||||
hasInteracted: true,
|
||||
lastInteraction: Date.now() - Math.floor(Math.random() * 3600000),
|
||||
playbackRate: 1,
|
||||
volume: 0.8
|
||||
}));
|
||||
|
||||
// 3. DRM license cache simulation
|
||||
sessionStorage.setItem('drm_licenses', JSON.stringify({
|
||||
widevine: {
|
||||
version: '4.10.2449.0',
|
||||
lastUpdate: Date.now() - Math.floor(Math.random() * 604800000),
|
||||
status: 'valid'
|
||||
}
|
||||
}));
|
||||
|
||||
// 4. Instagram session tokens
|
||||
const csrfToken = document.querySelector('[name="csrfmiddlewaretoken"]')?.value ||
|
||||
document.querySelector('meta[name="csrf-token"]')?.content ||
|
||||
'missing';
|
||||
|
||||
if (csrfToken !== 'missing') {
|
||||
sessionStorage.setItem('csrf_token', csrfToken);
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
try:
|
||||
self.page.evaluate(session_script)
|
||||
logger.info("Video session data injected successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to inject video session data: {e}")
|
||||
|
||||
def simulate_user_interaction(self) -> None:
|
||||
"""Simuliert authentische Benutzerinteraktion"""
|
||||
|
||||
try:
|
||||
# Random mouse movements
|
||||
for _ in range(3):
|
||||
x = random.randint(100, 800)
|
||||
y = random.randint(100, 600)
|
||||
self.page.mouse.move(x, y)
|
||||
time.sleep(random.uniform(0.1, 0.3))
|
||||
|
||||
# Random scroll
|
||||
self.page.mouse.wheel(0, random.randint(-200, 200))
|
||||
time.sleep(random.uniform(0.2, 0.5))
|
||||
|
||||
# Click somewhere safe (not on video)
|
||||
self.page.click('body', position={'x': random.randint(50, 100), 'y': random.randint(50, 100)})
|
||||
time.sleep(random.uniform(0.3, 0.7))
|
||||
|
||||
logger.info("User interaction simulation completed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to simulate user interaction: {e}")
|
||||
|
||||
def check_video_errors(self) -> Dict[str, Any]:
|
||||
"""Überprüft Video-Fehler und DRM-Status"""
|
||||
|
||||
try:
|
||||
result = self.page.evaluate("""
|
||||
() => {
|
||||
const errors = [];
|
||||
const diagnostics = {
|
||||
drm_support: false,
|
||||
media_source: false,
|
||||
codec_support: {},
|
||||
video_elements: 0,
|
||||
error_messages: []
|
||||
};
|
||||
|
||||
// Check for DRM support
|
||||
if (navigator.requestMediaKeySystemAccess) {
|
||||
diagnostics.drm_support = true;
|
||||
}
|
||||
|
||||
// Check Media Source Extensions
|
||||
if (window.MediaSource) {
|
||||
diagnostics.media_source = true;
|
||||
diagnostics.codec_support = {
|
||||
h264: MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E"'),
|
||||
vp9: MediaSource.isTypeSupported('video/webm; codecs="vp9"'),
|
||||
aac: MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"')
|
||||
};
|
||||
}
|
||||
|
||||
// Count video elements
|
||||
diagnostics.video_elements = document.querySelectorAll('video').length;
|
||||
|
||||
// Look for error messages
|
||||
const errorElements = document.querySelectorAll('[class*="error"], [class*="fail"]');
|
||||
errorElements.forEach(el => {
|
||||
if (el.textContent.includes('Video') || el.textContent.includes('video')) {
|
||||
diagnostics.error_messages.push(el.textContent.trim());
|
||||
}
|
||||
});
|
||||
|
||||
// Console errors
|
||||
const consoleErrors = [];
|
||||
const originalConsoleError = console.error;
|
||||
console.error = function(...args) {
|
||||
consoleErrors.push(args.join(' '));
|
||||
originalConsoleError.apply(console, arguments);
|
||||
};
|
||||
|
||||
return {
|
||||
diagnostics,
|
||||
console_errors: consoleErrors,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
""")
|
||||
|
||||
logger.info(f"Video diagnostics: {result}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Video error check failed: {e}")
|
||||
return {}
|
||||
127
browser/playwright_extensions.py
Normale Datei
127
browser/playwright_extensions.py
Normale Datei
@ -0,0 +1,127 @@
|
||||
# browser/playwright_extensions.py
|
||||
|
||||
"""
|
||||
Erweiterungen für den PlaywrightManager - Fügt zusätzliche Funktionalität hinzu
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from browser.fingerprint_protection import FingerprintProtection
|
||||
|
||||
logger = logging.getLogger("playwright_extensions")
|
||||
|
||||
class PlaywrightExtensions:
|
||||
"""
|
||||
Erweiterungsklasse für den PlaywrightManager.
|
||||
Bietet zusätzliche Funktionalität, ohne die Hauptklasse zu verändern.
|
||||
"""
|
||||
|
||||
def __init__(self, playwright_manager):
|
||||
"""
|
||||
Initialisiert die Erweiterungsklasse.
|
||||
|
||||
Args:
|
||||
playwright_manager: Eine Instanz des PlaywrightManager
|
||||
"""
|
||||
self.playwright_manager = playwright_manager
|
||||
self.fingerprint_protection = None
|
||||
self.enhanced_stealth_enabled = False
|
||||
|
||||
def enable_enhanced_fingerprint_protection(self, config: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""
|
||||
Aktiviert den erweiterten Fingerprint-Schutz.
|
||||
|
||||
Args:
|
||||
config: Optionale Konfiguration für den Fingerprint-Schutz
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Sicherstellen, dass der Browser gestartet wurde
|
||||
if not hasattr(self.playwright_manager, 'context') or self.playwright_manager.context is None:
|
||||
logger.warning("Browser muss zuerst gestartet werden, bevor der Fingerprint-Schutz aktiviert werden kann")
|
||||
return False
|
||||
|
||||
# Basis-Stealth-Konfiguration aus dem PlaywrightManager verwenden
|
||||
stealth_config = getattr(self.playwright_manager, 'stealth_config', {})
|
||||
|
||||
# Mit der benutzerdefinierten Konfiguration erweitern, falls vorhanden
|
||||
if config:
|
||||
stealth_config.update(config)
|
||||
|
||||
# Fingerprint-Schutz initialisieren
|
||||
self.fingerprint_protection = FingerprintProtection(
|
||||
context=self.playwright_manager.context,
|
||||
stealth_config=stealth_config
|
||||
)
|
||||
|
||||
# Schutzmaßnahmen auf den Kontext anwenden
|
||||
self.fingerprint_protection.apply_to_context()
|
||||
|
||||
# Status aktualisieren
|
||||
self.enhanced_stealth_enabled = True
|
||||
|
||||
logger.info("Erweiterter Fingerprint-Schutz aktiviert")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktivieren des erweiterten Fingerprint-Schutzes: {e}")
|
||||
return False
|
||||
|
||||
def rotate_fingerprint(self, noise_level: Optional[float] = None) -> bool:
|
||||
"""
|
||||
Rotiert den Browser-Fingerprint.
|
||||
|
||||
Args:
|
||||
noise_level: Optionales neues Rauschniveau (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self.enhanced_stealth_enabled or self.fingerprint_protection is None:
|
||||
logger.warning("Erweiterter Fingerprint-Schutz ist nicht aktiviert")
|
||||
return False
|
||||
|
||||
return self.fingerprint_protection.rotate_fingerprint(noise_level)
|
||||
|
||||
def get_fingerprint_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt den aktuellen Status des Fingerprint-Schutzes zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Status des Fingerprint-Schutzes
|
||||
"""
|
||||
if not self.enhanced_stealth_enabled or self.fingerprint_protection is None:
|
||||
return {"active": False, "message": "Erweiterter Fingerprint-Schutz ist nicht aktiviert"}
|
||||
|
||||
return self.fingerprint_protection.get_fingerprint_status()
|
||||
|
||||
def hook_into_playwright_manager(self) -> None:
|
||||
"""
|
||||
Hängt die Erweiterungsmethoden an den PlaywrightManager.
|
||||
"""
|
||||
if not self.playwright_manager:
|
||||
logger.error("Kein PlaywrightManager zum Anhängen der Erweiterungen")
|
||||
return
|
||||
|
||||
# Originalstart-Methode speichern
|
||||
original_start = self.playwright_manager.start
|
||||
|
||||
# Die start-Methode überschreiben, um den Fingerprint-Schutz automatisch zu aktivieren
|
||||
def enhanced_start(*args, **kwargs):
|
||||
result = original_start(*args, **kwargs)
|
||||
|
||||
# Wenn start erfolgreich war und erweiterter Schutz aktiviert ist,
|
||||
# wenden wir den Fingerprint-Schutz auf den neuen Kontext an
|
||||
if result and self.enhanced_stealth_enabled and self.fingerprint_protection:
|
||||
self.fingerprint_protection.set_context(self.playwright_manager.context)
|
||||
self.fingerprint_protection.apply_to_context()
|
||||
|
||||
return result
|
||||
|
||||
# Methoden dynamisch zum PlaywrightManager hinzufügen
|
||||
self.playwright_manager.enable_enhanced_fingerprint_protection = self.enable_enhanced_fingerprint_protection
|
||||
self.playwright_manager.rotate_fingerprint = self.rotate_fingerprint
|
||||
self.playwright_manager.get_fingerprint_status = self.get_fingerprint_status
|
||||
self.playwright_manager.start = enhanced_start
|
||||
906
browser/playwright_manager.py
Normale Datei
906
browser/playwright_manager.py
Normale Datei
@ -0,0 +1,906 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
headless: bool = False,
|
||||
proxy: Optional[Dict[str, str]] = None,
|
||||
browser_type: str = "chromium",
|
||||
user_agent: Optional[str] = None,
|
||||
screenshots_dir: str = "screenshots",
|
||||
slowmo: int = 0,
|
||||
window_position: Optional[Tuple[int, int]] = None):
|
||||
"""
|
||||
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.user_agent = user_agent
|
||||
self.screenshots_dir = screenshots_dir
|
||||
self.slowmo = slowmo
|
||||
self.window_position = window_position
|
||||
|
||||
# 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 Stealth-Konfigurationen
|
||||
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 start(self) -> Page:
|
||||
"""
|
||||
Startet die Playwright-Sitzung und gibt die Browser-Seite zurück.
|
||||
|
||||
Returns:
|
||||
Page: Die Browser-Seite
|
||||
"""
|
||||
if self.page is not None:
|
||||
return self.page
|
||||
|
||||
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',
|
||||
])
|
||||
|
||||
# Browser-Launch-Optionen
|
||||
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}'
|
||||
])
|
||||
|
||||
# Browser 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)
|
||||
|
||||
# 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}"))
|
||||
|
||||
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)
|
||||
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 wait_for_selector(self, selector: str, timeout: int = 30000) -> Optional[ElementHandle]:
|
||||
"""
|
||||
Wartet auf ein Element mit dem angegebenen Selektor.
|
||||
|
||||
Args:
|
||||
selector: CSS- oder XPath-Selektor
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
Optional[ElementHandle]: Das Element oder None, wenn nicht gefunden
|
||||
"""
|
||||
if self.page is None:
|
||||
raise ValueError("Browser nicht gestartet. Rufe zuerst start() auf.")
|
||||
|
||||
try:
|
||||
element = self.page.wait_for_selector(selector, timeout=timeout)
|
||||
return element
|
||||
except Exception as e:
|
||||
logger.warning(f"Element nicht gefunden: {selector} - {e}")
|
||||
return None
|
||||
|
||||
def fill_form_field(self, selector: str, value: str, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Füllt ein Formularfeld aus.
|
||||
|
||||
Args:
|
||||
selector: Selektor für das Feld
|
||||
value: Einzugebender Wert
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Auf Element warten
|
||||
element = self.wait_for_selector(selector, timeout)
|
||||
if not element:
|
||||
return False
|
||||
|
||||
# Element fokussieren
|
||||
element.focus()
|
||||
time.sleep(random.uniform(0.1, 0.3))
|
||||
|
||||
# Vorhandenen Text löschen (optional)
|
||||
current_value = element.evaluate("el => el.value")
|
||||
if current_value:
|
||||
element.fill("")
|
||||
time.sleep(random.uniform(0.1, 0.2))
|
||||
|
||||
# Text menschenähnlich eingeben
|
||||
for char in value:
|
||||
element.type(char, delay=random.uniform(20, 100))
|
||||
time.sleep(random.uniform(0.01, 0.05))
|
||||
|
||||
logger.info(f"Feld {selector} gefüllt mit: {value}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen von {selector}: {e}")
|
||||
key = f"fill_{selector}"
|
||||
return self._retry_action(key, lambda: self.fill_form_field(selector, value, timeout))
|
||||
|
||||
def click_element(self, selector: str, force: bool = False, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Klickt auf ein Element mit Anti-Bot-Bypass-Strategien.
|
||||
|
||||
Args:
|
||||
selector: Selektor für das Element
|
||||
force: Force-Click verwenden
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Auf Element warten
|
||||
element = self.wait_for_selector(selector, timeout)
|
||||
if not element:
|
||||
return False
|
||||
|
||||
# Scroll zum Element
|
||||
self.page.evaluate("element => element.scrollIntoView({ behavior: 'smooth', block: 'center' })", element)
|
||||
time.sleep(random.uniform(0.3, 0.7))
|
||||
|
||||
# Menschenähnliches Verhalten - leichte Verzögerung vor dem Klick
|
||||
time.sleep(random.uniform(0.2, 0.5))
|
||||
|
||||
# Element klicken
|
||||
element.click(force=force, delay=random.uniform(20, 100))
|
||||
|
||||
logger.info(f"Element geklickt: {selector}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Klicken auf {selector}: {e}")
|
||||
# Bei Fehlern verwende robuste Click-Strategien
|
||||
return self.robust_click(selector, timeout)
|
||||
|
||||
def robust_click(self, selector: str, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Robuste Click-Methode mit mehreren Anti-Bot-Bypass-Strategien.
|
||||
Speziell für Instagram's Click-Interceptors entwickelt.
|
||||
|
||||
Args:
|
||||
selector: Selektor für das Element
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
logger.info(f"Verwende robuste Click-Strategien für: {selector}")
|
||||
|
||||
strategies = [
|
||||
# Strategie 1: Standard Playwright Click
|
||||
lambda: self._strategy_standard_click(selector, timeout),
|
||||
|
||||
# Strategie 2: Force Click
|
||||
lambda: self._strategy_force_click(selector, timeout),
|
||||
|
||||
# Strategie 3: JavaScript Event Dispatch
|
||||
lambda: self._strategy_javascript_click(selector),
|
||||
|
||||
# Strategie 4: Overlay-Entfernung + Click
|
||||
lambda: self._strategy_remove_overlays_click(selector, timeout),
|
||||
|
||||
# Strategie 5: Focus + Enter (für Buttons/Links)
|
||||
lambda: self._strategy_focus_and_enter(selector),
|
||||
|
||||
# Strategie 6: Mouse Position Click
|
||||
lambda: self._strategy_coordinate_click(selector)
|
||||
]
|
||||
|
||||
for i, strategy in enumerate(strategies, 1):
|
||||
try:
|
||||
logger.debug(f"Versuche Click-Strategie {i} für {selector}")
|
||||
if strategy():
|
||||
logger.info(f"Click erfolgreich mit Strategie {i} für {selector}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Strategie {i} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
logger.error(f"Alle Click-Strategien fehlgeschlagen für {selector}")
|
||||
return False
|
||||
|
||||
def _strategy_standard_click(self, selector: str, timeout: int) -> bool:
|
||||
"""Strategie 1: Standard Playwright Click"""
|
||||
element = self.wait_for_selector(selector, timeout)
|
||||
if not element:
|
||||
return False
|
||||
element.click()
|
||||
return True
|
||||
|
||||
def _strategy_force_click(self, selector: str, timeout: int) -> bool:
|
||||
"""Strategie 2: Force Click um Event-Blockierungen zu umgehen"""
|
||||
element = self.wait_for_selector(selector, timeout)
|
||||
if not element:
|
||||
return False
|
||||
element.click(force=True)
|
||||
return True
|
||||
|
||||
def _strategy_javascript_click(self, selector: str) -> bool:
|
||||
"""Strategie 3: JavaScript Event Dispatch um Overlays zu umgehen"""
|
||||
script = f"""
|
||||
(function() {{
|
||||
const element = document.querySelector('{selector}');
|
||||
if (!element) return false;
|
||||
|
||||
// Erstelle und sende Click-Event
|
||||
const event = new MouseEvent('click', {{
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
detail: 1,
|
||||
button: 0,
|
||||
buttons: 1
|
||||
}});
|
||||
|
||||
element.dispatchEvent(event);
|
||||
|
||||
// Zusätzlich: Focus und Click Events
|
||||
element.focus();
|
||||
|
||||
const clickEvent = new Event('click', {{
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}});
|
||||
element.dispatchEvent(clickEvent);
|
||||
|
||||
return true;
|
||||
}})();
|
||||
"""
|
||||
|
||||
return self.page.evaluate(script)
|
||||
|
||||
def _strategy_remove_overlays_click(self, selector: str, timeout: int) -> bool:
|
||||
"""Strategie 4: Entferne Click-Interceptors und klicke dann"""
|
||||
# Entferne Overlays die Click-Events abfangen
|
||||
self._remove_click_interceptors()
|
||||
|
||||
# Warte kurz damit DOM-Änderungen wirksam werden
|
||||
time.sleep(0.2)
|
||||
|
||||
# Jetzt normaler Click
|
||||
element = self.wait_for_selector(selector, timeout)
|
||||
if not element:
|
||||
return False
|
||||
element.click()
|
||||
return True
|
||||
|
||||
def _strategy_focus_and_enter(self, selector: str) -> bool:
|
||||
"""Strategie 5: Focus Element und verwende Enter-Taste"""
|
||||
script = f"""
|
||||
(function() {{
|
||||
const element = document.querySelector('{selector}');
|
||||
if (!element) return false;
|
||||
|
||||
// Element fokussieren
|
||||
element.focus();
|
||||
element.scrollIntoView({{ block: 'center' }});
|
||||
|
||||
// Enter-Event senden
|
||||
const enterEvent = new KeyboardEvent('keydown', {{
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
which: 13,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}});
|
||||
|
||||
element.dispatchEvent(enterEvent);
|
||||
|
||||
// Zusätzlich keyup Event
|
||||
const keyupEvent = new KeyboardEvent('keyup', {{
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
which: 13,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}});
|
||||
|
||||
element.dispatchEvent(keyupEvent);
|
||||
|
||||
return true;
|
||||
}})();
|
||||
"""
|
||||
|
||||
return self.page.evaluate(script)
|
||||
|
||||
def _strategy_coordinate_click(self, selector: str) -> bool:
|
||||
"""Strategie 6: Click auf Koordinaten des Elements"""
|
||||
try:
|
||||
element = self.page.locator(selector).first
|
||||
if not element.is_visible():
|
||||
return False
|
||||
|
||||
# Hole Element-Position
|
||||
box = element.bounding_box()
|
||||
if not box:
|
||||
return False
|
||||
|
||||
# Klicke in die Mitte des Elements
|
||||
x = box['x'] + box['width'] / 2
|
||||
y = box['y'] + box['height'] / 2
|
||||
|
||||
self.page.mouse.click(x, y)
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _remove_click_interceptors(self) -> None:
|
||||
"""
|
||||
Entfernt invisible Overlays die Click-Events abfangen.
|
||||
Speziell für Instagram's Anti-Bot-Maßnahmen entwickelt.
|
||||
"""
|
||||
script = """
|
||||
(function() {
|
||||
console.log('AccountForger: Entferne Click-Interceptors...');
|
||||
|
||||
// Liste typischer Instagram Click-Interceptor Klassen
|
||||
const interceptorSelectors = [
|
||||
// Instagram's bekannte Interceptor-Klassen
|
||||
'.x1lliihq.x1plvlek.xryxfnj',
|
||||
'.x1n2onr6.xzkaem6',
|
||||
'span[dir="auto"]',
|
||||
|
||||
// Allgemeine Interceptor-Eigenschaften
|
||||
'[style*="pointer-events: all"]',
|
||||
'[style*="position: absolute"]',
|
||||
'[style*="z-index"]'
|
||||
];
|
||||
|
||||
let removedCount = 0;
|
||||
|
||||
// Entferne Interceptor-Elemente
|
||||
interceptorSelectors.forEach(selector => {
|
||||
try {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach(el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
|
||||
// Prüfe ob Element ein Click-Interceptor ist
|
||||
const isInterceptor = (
|
||||
style.pointerEvents === 'all' ||
|
||||
(style.position === 'absolute' && parseInt(style.zIndex) > 1000) ||
|
||||
(el.offsetWidth > 0 && el.offsetHeight > 0 &&
|
||||
el.textContent.trim() === '' &&
|
||||
style.backgroundColor === 'rgba(0, 0, 0, 0)')
|
||||
);
|
||||
|
||||
if (isInterceptor) {
|
||||
// Deaktiviere Pointer-Events
|
||||
el.style.pointerEvents = 'none';
|
||||
el.style.display = 'none';
|
||||
el.style.visibility = 'hidden';
|
||||
removedCount++;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Fehler beim Entfernen von Interceptors:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// Zusätzlich: Entferne alle unsichtbaren absolute Elemente die über anderen liegen
|
||||
const allElements = document.querySelectorAll('*');
|
||||
allElements.forEach(el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
|
||||
if (style.position === 'absolute' || style.position === 'fixed') {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
// Prüfe ob Element unsichtbar aber vorhanden ist
|
||||
if (rect.width > 0 && rect.height > 0 &&
|
||||
style.opacity !== '0' &&
|
||||
style.visibility !== 'hidden' &&
|
||||
el.textContent.trim() === '' &&
|
||||
parseInt(style.zIndex) > 10) {
|
||||
|
||||
el.style.pointerEvents = 'none';
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`AccountForger: ${removedCount} Click-Interceptors entfernt`);
|
||||
|
||||
// Markiere dass Interceptors entfernt wurden
|
||||
window.__accountforge_interceptors_removed = true;
|
||||
|
||||
return removedCount;
|
||||
})();
|
||||
"""
|
||||
|
||||
try:
|
||||
removed_count = self.page.evaluate(script)
|
||||
if removed_count > 0:
|
||||
logger.info(f"Click-Interceptors entfernt: {removed_count}")
|
||||
else:
|
||||
logger.debug("Keine Click-Interceptors gefunden")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Entfernen von Click-Interceptors: {e}")
|
||||
|
||||
def select_option(self, selector: str, value: str, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Wählt eine Option aus einem Dropdown-Menü.
|
||||
|
||||
Args:
|
||||
selector: Selektor für das Dropdown
|
||||
value: Wert oder sichtbarer Text der Option
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Auf Element warten
|
||||
element = self.wait_for_selector(selector, timeout)
|
||||
if not element:
|
||||
return False
|
||||
|
||||
# Option auswählen
|
||||
self.page.select_option(selector, value=value)
|
||||
|
||||
logger.info(f"Option '{value}' ausgewählt in {selector}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Auswahl von '{value}' in {selector}: {e}")
|
||||
key = f"select_{selector}"
|
||||
return self._retry_action(key, lambda: self.select_option(selector, value, timeout))
|
||||
|
||||
def is_element_visible(self, selector: str, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Prüft, ob ein Element sichtbar ist.
|
||||
|
||||
Args:
|
||||
selector: Selektor für das Element
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True wenn sichtbar, False sonst
|
||||
"""
|
||||
try:
|
||||
element = self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
return element is not None
|
||||
except:
|
||||
return False
|
||||
|
||||
def take_screenshot(self, name: str = None) -> str:
|
||||
"""
|
||||
Erstellt einen Screenshot der aktuellen Seite.
|
||||
|
||||
Args:
|
||||
name: Name für den Screenshot (ohne Dateierweiterung)
|
||||
|
||||
Returns:
|
||||
str: Pfad zum erstellten Screenshot
|
||||
"""
|
||||
if self.page is None:
|
||||
raise ValueError("Browser nicht gestartet. Rufe zuerst start() auf.")
|
||||
|
||||
timestamp = int(time.time())
|
||||
filename = f"{name}_{timestamp}.png" if name else f"screenshot_{timestamp}.png"
|
||||
path = os.path.join(self.screenshots_dir, filename)
|
||||
|
||||
self.page.screenshot(path=path, full_page=True)
|
||||
logger.info(f"Screenshot erstellt: {path}")
|
||||
return path
|
||||
|
||||
def _retry_action(self, key: str, action_func, max_retries: int = 3) -> bool:
|
||||
"""
|
||||
Wiederholt eine Aktion bei Fehler.
|
||||
|
||||
Args:
|
||||
key: Eindeutiger Schlüssel für die Aktion
|
||||
action_func: Funktion, die ausgeführt werden soll
|
||||
max_retries: Maximale Anzahl der Wiederholungen
|
||||
|
||||
Returns:
|
||||
bool: Ergebnis der Aktion
|
||||
"""
|
||||
if key not in self.retry_counter:
|
||||
self.retry_counter[key] = 0
|
||||
|
||||
self.retry_counter[key] += 1
|
||||
|
||||
if self.retry_counter[key] <= max_retries:
|
||||
logger.info(f"Wiederhole Aktion {key} (Versuch {self.retry_counter[key]} von {max_retries})")
|
||||
time.sleep(random.uniform(0.5, 1.0))
|
||||
return action_func()
|
||||
else:
|
||||
logger.warning(f"Maximale Anzahl von Wiederholungen für {key} erreicht")
|
||||
self.retry_counter[key] = 0
|
||||
return False
|
||||
|
||||
def apply_protection(self, protection_style: Optional[BrowserProtectionStyle] = None) -> None:
|
||||
"""
|
||||
Wendet Browser-Schutz an, um versehentliche Benutzerinteraktionen zu verhindern.
|
||||
|
||||
Args:
|
||||
protection_style: Konfiguration für den Schutzstil. Verwendet Standardwerte wenn None.
|
||||
"""
|
||||
if self.page is None:
|
||||
raise ValueError("Browser nicht gestartet. Rufe zuerst start() auf.")
|
||||
|
||||
if protection_style is None:
|
||||
protection_style = BrowserProtectionStyle()
|
||||
|
||||
# Speichere den Stil für spätere Wiederanwendung
|
||||
self.protection_style = protection_style
|
||||
|
||||
# Wende Schutz initial an
|
||||
self.protection_service.protect_browser(self.page, protection_style)
|
||||
self.protection_applied = True
|
||||
|
||||
# Registriere Event-Handler für Seitenwechsel
|
||||
self._setup_protection_listeners()
|
||||
|
||||
logger.info(f"Browser-Schutz angewendet mit Level: {protection_style.level.value}")
|
||||
|
||||
def _setup_protection_listeners(self) -> None:
|
||||
"""Setzt Event-Listener auf, um Schutz bei Seitenwechsel neu anzuwenden."""
|
||||
if self.page is None:
|
||||
return
|
||||
|
||||
# Bei Navigation (Seitenwechsel) Schutz neu anwenden
|
||||
def on_navigation():
|
||||
if self.protection_applied and self.protection_style:
|
||||
# Kurz warten bis neue Seite geladen ist
|
||||
self.page.wait_for_load_state("domcontentloaded")
|
||||
# Schutz neu anwenden
|
||||
self.protection_service.protect_browser(self.page, self.protection_style)
|
||||
logger.debug("Browser-Schutz nach Navigation neu angewendet")
|
||||
|
||||
# Registriere Handler für verschiedene Events
|
||||
self.page.on("framenavigated", lambda frame: on_navigation() if frame == self.page.main_frame else None)
|
||||
|
||||
# Zusätzlich: Wende Schutz bei DOM-Änderungen neu an
|
||||
self.context.add_init_script("""
|
||||
// Überwache DOM-Änderungen und wende Schutz neu an wenn nötig
|
||||
const observer = new MutationObserver(() => {
|
||||
const shield = document.getElementById('accountforge-shield');
|
||||
if (!shield && window.__accountforge_protection) {
|
||||
// Schutz wurde entfernt, wende neu an
|
||||
setTimeout(() => {
|
||||
if (!document.getElementById('accountforge-shield')) {
|
||||
eval(window.__accountforge_protection);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
""")
|
||||
|
||||
def remove_protection(self) -> None:
|
||||
"""Entfernt den Browser-Schutz."""
|
||||
if self.page is None or not self.protection_applied:
|
||||
return
|
||||
|
||||
self.protection_service.remove_protection(self.page)
|
||||
self.protection_applied = False
|
||||
self.protection_style = None
|
||||
logger.info("Browser-Schutz entfernt")
|
||||
|
||||
def close(self):
|
||||
"""Schließt den Browser und gibt Ressourcen frei."""
|
||||
try:
|
||||
# Entferne Schutz vor dem Schließen
|
||||
if self.protection_applied:
|
||||
self.remove_protection()
|
||||
|
||||
# Seite erst schließen, dann Kontext, dann Browser, dann Playwright
|
||||
if self.page:
|
||||
try:
|
||||
self.page.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Schließen der Page: {e}")
|
||||
self.page = None
|
||||
|
||||
if self.context:
|
||||
try:
|
||||
self.context.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Schließen des Context: {e}")
|
||||
self.context = None
|
||||
|
||||
if self.browser:
|
||||
try:
|
||||
self.browser.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Schließen des Browsers: {e}")
|
||||
self.browser = None
|
||||
|
||||
# Playwright stop mit Retry-Logik
|
||||
if self.playwright:
|
||||
try:
|
||||
self.playwright.stop()
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Stoppen von Playwright: {e}")
|
||||
# Versuche force stop
|
||||
try:
|
||||
import time
|
||||
time.sleep(0.5) # Kurz warten
|
||||
self.playwright.stop()
|
||||
except Exception as e2:
|
||||
logger.error(f"Force stop fehlgeschlagen: {e2}")
|
||||
self.playwright = None
|
||||
|
||||
logger.info("Browser-Sitzung erfolgreich geschlossen")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Schließen des Browsers: {e}")
|
||||
# Versuche Ressourcen trotzdem zu nullen
|
||||
self.page = None
|
||||
self.context = None
|
||||
self.browser = None
|
||||
self.playwright = None
|
||||
|
||||
def __enter__(self):
|
||||
"""Kontext-Manager-Eintritt."""
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Kontext-Manager-Austritt."""
|
||||
self.close()
|
||||
|
||||
|
||||
# Beispielnutzung, wenn direkt ausgeführt
|
||||
if __name__ == "__main__":
|
||||
# Konfiguriere Logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Beispiel für einen Proxy (ohne Anmeldedaten)
|
||||
proxy_config = {
|
||||
"server": "http://example-proxy.com:8080"
|
||||
}
|
||||
|
||||
# Browser starten und zu einer Seite navigieren
|
||||
with PlaywrightManager(headless=False) as manager:
|
||||
manager.navigate_to("https://www.instagram.com")
|
||||
time.sleep(5) # Kurze Pause zum Anzeigen der Seite
|
||||
216
browser/stealth_config.py
Normale Datei
216
browser/stealth_config.py
Normale Datei
@ -0,0 +1,216 @@
|
||||
"""
|
||||
Stealth-Konfiguration für Playwright - Anti-Bot-Erkennung
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("stealth_config")
|
||||
|
||||
class StealthConfig:
|
||||
"""
|
||||
Konfiguriert Anti-Bot-Erkennungs-Einstellungen für Playwright.
|
||||
Generiert und verwaltet verschiedene Fingerprint-Einstellungen.
|
||||
"""
|
||||
|
||||
# Standardwerte für User-Agents
|
||||
CHROME_DESKTOP_AGENTS = [
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
||||
]
|
||||
|
||||
MOBILE_AGENTS = [
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/135.0.0.0 Mobile/15E148 Safari/604.1"
|
||||
]
|
||||
|
||||
# Plattformen
|
||||
PLATFORMS = {
|
||||
"windows": "Win32",
|
||||
"macos": "MacIntel",
|
||||
"linux": "Linux x86_64",
|
||||
"android": "Linux armv8l",
|
||||
"ios": "iPhone"
|
||||
}
|
||||
|
||||
# Browser-Sprachen
|
||||
LANGUAGES = [
|
||||
"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"de-DE,de;q=0.9,en;q=0.8",
|
||||
"de;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"en-US,en;q=0.9,de;q=0.8"
|
||||
]
|
||||
|
||||
# Zeitzone für Deutschland
|
||||
TIMEZONE_ID = "Europe/Berlin"
|
||||
|
||||
def __init__(self, config_dir: str = None):
|
||||
"""
|
||||
Initialisiert die Stealth-Konfiguration.
|
||||
|
||||
Args:
|
||||
config_dir: Verzeichnis für Konfigurationsdateien
|
||||
"""
|
||||
self.config_dir = config_dir or os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config")
|
||||
os.makedirs(self.config_dir, exist_ok=True)
|
||||
|
||||
self.config_path = os.path.join(self.config_dir, "stealth_config.json")
|
||||
|
||||
# Lade benutzerdefinierte User-Agents, falls vorhanden
|
||||
self.user_agents = self._load_user_agents()
|
||||
|
||||
# Lade gespeicherte Konfiguration oder erstelle eine neue
|
||||
self.config = self._load_or_create_config()
|
||||
|
||||
def _load_user_agents(self) -> Dict[str, List[str]]:
|
||||
"""Lädt benutzerdefinierte User-Agents aus der Konfigurationsdatei."""
|
||||
user_agents_path = os.path.join(self.config_dir, "user_agents.json")
|
||||
|
||||
if os.path.exists(user_agents_path):
|
||||
try:
|
||||
with open(user_agents_path, 'r', encoding='utf-8') as f:
|
||||
agents = json.load(f)
|
||||
|
||||
if isinstance(agents, dict) and "desktop" in agents and "mobile" in agents:
|
||||
return agents
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Laden von user_agents.json: {e}")
|
||||
|
||||
# Standardwerte zurückgeben
|
||||
return {
|
||||
"desktop": self.CHROME_DESKTOP_AGENTS,
|
||||
"mobile": self.MOBILE_AGENTS
|
||||
}
|
||||
|
||||
def _load_or_create_config(self) -> Dict[str, Any]:
|
||||
"""Lädt die Konfiguration oder erstellt eine neue, falls keine existiert."""
|
||||
if os.path.exists(self.config_path):
|
||||
try:
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
logger.info("Stealth-Konfiguration geladen")
|
||||
return config
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte Stealth-Konfiguration nicht laden: {e}")
|
||||
|
||||
# Erstelle eine neue Konfiguration
|
||||
config = self.generate_config()
|
||||
self.save_config(config)
|
||||
return config
|
||||
|
||||
def generate_config(self, device_type: str = "desktop") -> Dict[str, Any]:
|
||||
"""
|
||||
Generiert eine neue Stealth-Konfiguration.
|
||||
|
||||
Args:
|
||||
device_type: "desktop" oder "mobile"
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Die generierte Konfiguration
|
||||
"""
|
||||
# Wähle Plattform und entsprechenden User-Agent
|
||||
if device_type == "mobile":
|
||||
platform_name = random.choice(["android", "ios"])
|
||||
user_agent = random.choice(self.user_agents["mobile"])
|
||||
else:
|
||||
# Wähle eine Plattform, die zum System passt
|
||||
system = platform.system().lower()
|
||||
if system == "darwin":
|
||||
platform_name = "macos"
|
||||
elif system == "windows":
|
||||
platform_name = "windows"
|
||||
else:
|
||||
platform_name = "linux"
|
||||
|
||||
user_agent = random.choice(self.user_agents["desktop"])
|
||||
|
||||
platform_value = self.PLATFORMS.get(platform_name, "Win32")
|
||||
|
||||
# Wähle weitere Konfigurationen
|
||||
config = {
|
||||
"user_agent": user_agent,
|
||||
"platform": platform_value,
|
||||
"vendor": "Google Inc." if "Chrome" in user_agent else "Apple Computer, Inc.",
|
||||
"accept_language": random.choice(self.LANGUAGES),
|
||||
"timezone_id": self.TIMEZONE_ID,
|
||||
"device_scale_factor": random.choice([1.0, 1.25, 1.5, 2.0]) if random.random() < 0.3 else 1.0,
|
||||
"color_depth": random.choice([24, 30, 48]),
|
||||
"hardware_concurrency": random.choice([2, 4, 8, 12, 16]),
|
||||
"device_memory": random.choice([2, 4, 8, 16]),
|
||||
"webdriver": False,
|
||||
"fingerprint_noise": True,
|
||||
"device_type": device_type
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
def save_config(self, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Speichert die Konfiguration in einer Datei.
|
||||
|
||||
Args:
|
||||
config: Die zu speichernde Konfiguration
|
||||
"""
|
||||
try:
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
logger.info(f"Stealth-Konfiguration gespeichert in: {self.config_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Speichern der Stealth-Konfiguration: {e}")
|
||||
|
||||
def get_config(self) -> Dict[str, Any]:
|
||||
"""Gibt die aktuelle Konfiguration zurück."""
|
||||
return self.config
|
||||
|
||||
def rotate_config(self, device_type: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Generiert eine neue Konfiguration und speichert sie.
|
||||
|
||||
Args:
|
||||
device_type: "desktop" oder "mobile", oder None für bestehenden Typ
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Die neue Konfiguration
|
||||
"""
|
||||
if device_type is None:
|
||||
device_type = self.config.get("device_type", "desktop")
|
||||
|
||||
self.config = self.generate_config(device_type)
|
||||
self.save_config(self.config)
|
||||
return self.config
|
||||
|
||||
def get_user_agent(self) -> str:
|
||||
"""Gibt den aktuellen User-Agent aus der Konfiguration zurück."""
|
||||
return self.config.get("user_agent", self.CHROME_DESKTOP_AGENTS[0])
|
||||
|
||||
|
||||
# Beispielnutzung, wenn direkt ausgeführt
|
||||
if __name__ == "__main__":
|
||||
# Konfiguriere Logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Beispiel für Konfigurationserstellung
|
||||
stealth = StealthConfig()
|
||||
|
||||
print("Aktuelle Konfiguration:")
|
||||
print(json.dumps(stealth.get_config(), indent=2))
|
||||
|
||||
print("\nNeue Desktop-Konfiguration:")
|
||||
desktop_config = stealth.rotate_config("desktop")
|
||||
print(json.dumps(desktop_config, indent=2))
|
||||
|
||||
print("\nNeue Mobile-Konfiguration:")
|
||||
mobile_config = stealth.rotate_config("mobile")
|
||||
print(json.dumps(mobile_config, indent=2))
|
||||
318
browser/video_stealth_enhancement.py
Normale Datei
318
browser/video_stealth_enhancement.py
Normale Datei
@ -0,0 +1,318 @@
|
||||
# Video Stealth Enhancement Module
|
||||
"""
|
||||
Erweiterte Video-spezifische Stealth-Maßnahmen für Instagram DRM-Schutz
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
logger = logging.getLogger("video_stealth_enhancement")
|
||||
|
||||
class VideoStealthEnhancement:
|
||||
"""Video-spezifische Anti-Detection und DRM-Schutz"""
|
||||
|
||||
def __init__(self, context: Any):
|
||||
self.context = context
|
||||
|
||||
def apply_video_stealth(self) -> None:
|
||||
"""Wendet erweiterte Video-Stealth-Maßnahmen an"""
|
||||
|
||||
# 1. DRM und Widevine Capability Spoofing
|
||||
drm_script = """
|
||||
() => {
|
||||
// Enhanced Widevine DRM Support
|
||||
if (!navigator.requestMediaKeySystemAccess) {
|
||||
navigator.requestMediaKeySystemAccess = function(keySystem, supportedConfigurations) {
|
||||
if (keySystem === 'com.widevine.alpha') {
|
||||
return Promise.resolve({
|
||||
keySystem: 'com.widevine.alpha',
|
||||
getConfiguration: () => ({
|
||||
initDataTypes: ['cenc'],
|
||||
audioCapabilities: [{contentType: 'audio/mp4; codecs="mp4a.40.2"'}],
|
||||
videoCapabilities: [{contentType: 'video/mp4; codecs="avc1.42E01E"'}],
|
||||
distinctiveIdentifier: 'optional',
|
||||
persistentState: 'optional'
|
||||
}),
|
||||
createMediaKeys: () => Promise.resolve({
|
||||
createSession: () => ({
|
||||
addEventListener: () => {},
|
||||
generateRequest: () => Promise.resolve(),
|
||||
update: () => Promise.resolve(),
|
||||
close: () => Promise.resolve()
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error('KeySystem not supported'));
|
||||
};
|
||||
}
|
||||
|
||||
// EME (Encrypted Media Extensions) Support
|
||||
Object.defineProperty(HTMLMediaElement.prototype, 'canPlayType', {
|
||||
value: function(type) {
|
||||
const supportedTypes = {
|
||||
'video/mp4; codecs="avc1.42E01E"': 'probably',
|
||||
'video/mp4; codecs="avc1.4D4015"': 'probably',
|
||||
'video/webm; codecs="vp8"': 'probably',
|
||||
'video/webm; codecs="vp9"': 'probably',
|
||||
'audio/mp4; codecs="mp4a.40.2"': 'probably',
|
||||
'audio/webm; codecs="opus"': 'probably'
|
||||
};
|
||||
return supportedTypes[type] || 'maybe';
|
||||
}
|
||||
});
|
||||
|
||||
// Enhanced Media Capabilities
|
||||
if (!navigator.mediaCapabilities) {
|
||||
navigator.mediaCapabilities = {
|
||||
decodingInfo: function(config) {
|
||||
return Promise.resolve({
|
||||
supported: true,
|
||||
smooth: true,
|
||||
powerEfficient: true
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# 2. Video Element Enhancement
|
||||
video_element_script = """
|
||||
() => {
|
||||
// Enhanced Video Element Support
|
||||
const originalCreateElement = document.createElement;
|
||||
document.createElement = function(tagName) {
|
||||
const element = originalCreateElement.call(this, tagName);
|
||||
|
||||
if (tagName.toLowerCase() === 'video') {
|
||||
// Override video properties for better compatibility
|
||||
Object.defineProperty(element, 'webkitDisplayingFullscreen', {
|
||||
get: () => false,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(element, 'webkitSupportsFullscreen', {
|
||||
get: () => true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(element, 'webkitDecodedFrameCount', {
|
||||
get: () => Math.floor(Math.random() * 1000) + 100,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(element, 'webkitDroppedFrameCount', {
|
||||
get: () => Math.floor(Math.random() * 10),
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Enhanced autoplay support
|
||||
Object.defineProperty(element, 'autoplay', {
|
||||
get: function() { return this._autoplay || false; },
|
||||
set: function(value) { this._autoplay = value; },
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
// User Activation API (required for autoplay)
|
||||
if (!navigator.userActivation) {
|
||||
Object.defineProperty(navigator, 'userActivation', {
|
||||
get: () => ({
|
||||
hasBeenActive: true,
|
||||
isActive: true
|
||||
}),
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# 3. Enhanced Media Devices
|
||||
media_devices_script = """
|
||||
() => {
|
||||
// Enhanced MediaDevices for Instagram
|
||||
if (navigator.mediaDevices) {
|
||||
const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices;
|
||||
navigator.mediaDevices.enumerateDevices = function() {
|
||||
return Promise.resolve([
|
||||
{
|
||||
deviceId: 'default',
|
||||
kind: 'audioinput',
|
||||
label: 'Default - Mikrofon (Realtek High Definition Audio)',
|
||||
groupId: 'group_audio_input'
|
||||
},
|
||||
{
|
||||
deviceId: 'communications',
|
||||
kind: 'audioinput',
|
||||
label: 'Kommunikation - Mikrofon (Realtek High Definition Audio)',
|
||||
groupId: 'group_audio_input'
|
||||
},
|
||||
{
|
||||
deviceId: 'default',
|
||||
kind: 'audiooutput',
|
||||
label: 'Standard - Lautsprecher (Realtek High Definition Audio)',
|
||||
groupId: 'group_audio_output'
|
||||
},
|
||||
{
|
||||
deviceId: 'communications',
|
||||
kind: 'audiooutput',
|
||||
label: 'Kommunikation - Lautsprecher (Realtek High Definition Audio)',
|
||||
groupId: 'group_audio_output'
|
||||
},
|
||||
{
|
||||
deviceId: 'video_device_1',
|
||||
kind: 'videoinput',
|
||||
label: 'HD-Webcam (USB)',
|
||||
groupId: 'group_video_input'
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
// Enhanced getUserMedia support
|
||||
if (!navigator.mediaDevices.getUserMedia) {
|
||||
navigator.mediaDevices.getUserMedia = function(constraints) {
|
||||
return Promise.resolve({
|
||||
getTracks: () => [],
|
||||
getAudioTracks: () => [],
|
||||
getVideoTracks: () => [],
|
||||
addTrack: () => {},
|
||||
removeTrack: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# 4. Instagram-spezifische Video Fixes
|
||||
instagram_video_script = """
|
||||
() => {
|
||||
// Instagram-specific video enhancements
|
||||
|
||||
// Simulate proper video loading behavior
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(input, init) {
|
||||
const url = typeof input === 'string' ? input : input.url;
|
||||
|
||||
// Enhance video CDN requests with proper headers
|
||||
if (url.includes('instagram.com') && (url.includes('.mp4') || url.includes('video'))) {
|
||||
const enhancedInit = {
|
||||
...init,
|
||||
headers: {
|
||||
...init?.headers,
|
||||
'Accept': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
|
||||
'Accept-Encoding': 'identity;q=1, *;q=0',
|
||||
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
'Range': 'bytes=0-',
|
||||
'Sec-Fetch-Dest': 'video',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Site': 'cross-site'
|
||||
}
|
||||
};
|
||||
return originalFetch.call(this, input, enhancedInit);
|
||||
}
|
||||
|
||||
return originalFetch.apply(this, arguments);
|
||||
};
|
||||
|
||||
// Override video error handling
|
||||
const originalAddEventListener = HTMLVideoElement.prototype.addEventListener;
|
||||
HTMLVideoElement.prototype.addEventListener = function(type, listener, options) {
|
||||
if (type === 'error' || type === 'abort') {
|
||||
// Wrap error listener to prevent video error displays
|
||||
const wrappedListener = function(event) {
|
||||
console.debug('AccountForger: Video event intercepted:', type);
|
||||
// Prevent error propagation for DRM-related issues
|
||||
if (event.target && event.target.error && event.target.error.code === 3) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
return listener.call(this, event);
|
||||
};
|
||||
return originalAddEventListener.call(this, type, wrappedListener, options);
|
||||
}
|
||||
return originalAddEventListener.call(this, type, listener, options);
|
||||
};
|
||||
|
||||
// Simulate proper video metrics
|
||||
Object.defineProperty(HTMLVideoElement.prototype, 'buffered', {
|
||||
get: function() {
|
||||
return {
|
||||
length: 1,
|
||||
start: () => 0,
|
||||
end: () => this.duration || 30
|
||||
};
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
# Alle Skripte anwenden
|
||||
scripts = [drm_script, video_element_script, media_devices_script, instagram_video_script]
|
||||
|
||||
for script in scripts:
|
||||
try:
|
||||
self.context.add_init_script(script)
|
||||
logger.debug("Video stealth script applied successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply video stealth script: {e}")
|
||||
|
||||
logger.info("Video stealth enhancement applied - DRM and Instagram compatibility enabled")
|
||||
|
||||
def validate_video_capabilities(self, page: Any) -> Dict[str, bool]:
|
||||
"""Validiert Video-Capabilities des Browsers"""
|
||||
try:
|
||||
result = page.evaluate("""
|
||||
() => {
|
||||
const results = {
|
||||
widevine_support: false,
|
||||
media_devices: false,
|
||||
video_codecs: false,
|
||||
user_activation: false,
|
||||
autoplay_policy: false
|
||||
};
|
||||
|
||||
// Check Widevine support
|
||||
if (navigator.requestMediaKeySystemAccess) {
|
||||
results.widevine_support = true;
|
||||
}
|
||||
|
||||
// Check MediaDevices
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
|
||||
results.media_devices = true;
|
||||
}
|
||||
|
||||
// Check Video Codecs
|
||||
const video = document.createElement('video');
|
||||
if (video.canPlayType('video/mp4; codecs="avc1.42E01E"') === 'probably') {
|
||||
results.video_codecs = true;
|
||||
}
|
||||
|
||||
// Check User Activation
|
||||
if (navigator.userActivation && navigator.userActivation.hasBeenActive) {
|
||||
results.user_activation = true;
|
||||
}
|
||||
|
||||
// Check Autoplay Policy
|
||||
results.autoplay_policy = true; // Always report as supported
|
||||
|
||||
return results;
|
||||
}
|
||||
""")
|
||||
|
||||
logger.info(f"Video capabilities validation: {result}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Video capabilities validation failed: {e}")
|
||||
return {}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren