Initial commit
Dieser Commit ist enthalten in:
0
browser/__init__.py
Normale Datei
0
browser/__init__.py
Normale Datei
BIN
browser/__pycache__/__init__.cpython-310.pyc
Normale Datei
BIN
browser/__pycache__/__init__.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
browser/__pycache__/__init__.cpython-313.pyc
Normale Datei
BIN
browser/__pycache__/__init__.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
browser/__pycache__/fingerprint_protection.cpython-310.pyc
Normale Datei
BIN
browser/__pycache__/fingerprint_protection.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
browser/__pycache__/fingerprint_protection.cpython-313.pyc
Normale Datei
BIN
browser/__pycache__/fingerprint_protection.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
browser/__pycache__/playwright_extensions.cpython-310.pyc
Normale Datei
BIN
browser/__pycache__/playwright_extensions.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
browser/__pycache__/playwright_extensions.cpython-313.pyc
Normale Datei
BIN
browser/__pycache__/playwright_extensions.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
browser/__pycache__/playwright_manager.cpython-310.pyc
Normale Datei
BIN
browser/__pycache__/playwright_manager.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
browser/__pycache__/playwright_manager.cpython-313.pyc
Normale Datei
BIN
browser/__pycache__/playwright_manager.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
721
browser/fingerprint_protection.py
Normale Datei
721
browser/fingerprint_protection.py
Normale Datei
@ -0,0 +1,721 @@
|
||||
# browser/fingerprint_protection.py
|
||||
|
||||
"""
|
||||
Schutz vor Browser-Fingerprinting - Erweiterte Methoden zum Schutz vor verschiedenen Fingerprinting-Techniken
|
||||
"""
|
||||
|
||||
import random
|
||||
import logging
|
||||
import json
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger("fingerprint_protection")
|
||||
|
||||
class FingerprintProtection:
|
||||
"""
|
||||
Bietet erweiterte Schutzmaßnahmen gegen verschiedene Browser-Fingerprinting-Techniken.
|
||||
Kann mit dem PlaywrightManager integriert werden, um die Anonymität zu verbessern.
|
||||
"""
|
||||
|
||||
def __init__(self, context=None, stealth_config=None):
|
||||
"""
|
||||
Initialisiert den Fingerprint-Schutz.
|
||||
|
||||
Args:
|
||||
context: Der Browser-Kontext, auf den die Schutzmaßnahmen angewendet werden sollen
|
||||
stealth_config: Konfigurationseinstellungen für das Stealth-Verhalten
|
||||
"""
|
||||
self.context = context
|
||||
self.stealth_config = stealth_config or {}
|
||||
self.scripts = []
|
||||
self.noise_level = self.stealth_config.get("noise_level", 0.5) # 0.0-1.0
|
||||
|
||||
# Standardwerte für Fingerprinting-Schutz
|
||||
self.defaults = {
|
||||
"webgl_vendor": "Google Inc. (Intel)",
|
||||
"webgl_renderer": "Intel Iris OpenGL Engine",
|
||||
"canvas_noise": True,
|
||||
"audio_noise": True,
|
||||
"webgl_noise": True,
|
||||
"hardware_concurrency": 8,
|
||||
"device_memory": 8,
|
||||
"timezone_id": "Europe/Berlin"
|
||||
}
|
||||
|
||||
# Einstellungen mit benutzerdefinierten Werten überschreiben
|
||||
for key, value in self.stealth_config.items():
|
||||
if key in self.defaults:
|
||||
self.defaults[key] = value
|
||||
|
||||
# Schutzmaßnahmen initialisieren
|
||||
self._init_protections()
|
||||
|
||||
def set_context(self, context):
|
||||
"""Setzt den Browser-Kontext nach der Initialisierung."""
|
||||
self.context = context
|
||||
|
||||
def _init_protections(self):
|
||||
"""Initialisiert alle Fingerprint-Schutzmaßnahmen."""
|
||||
self._init_canvas_protection()
|
||||
self._init_webgl_protection()
|
||||
self._init_audio_protection()
|
||||
self._init_navigator_protection()
|
||||
self._init_misc_protections()
|
||||
|
||||
def _init_canvas_protection(self):
|
||||
"""
|
||||
Initialisiert den Schutz gegen Canvas-Fingerprinting.
|
||||
Dies modifiziert das Canvas-Element, um leicht abweichende Werte zurückzugeben.
|
||||
"""
|
||||
script = """
|
||||
() => {
|
||||
// Originalmethoden speichern
|
||||
const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
||||
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
||||
const originalToBlob = HTMLCanvasElement.prototype.toBlob;
|
||||
|
||||
// Funktion zum Hinzufügen von Rauschen zu Bilddaten
|
||||
const addNoise = (data, noise) => {
|
||||
const noiseFactor = noise || 0.03; // Standardwert für das Rauschlevel
|
||||
|
||||
// Nur auf Canvas über Mindestgröße anwenden, um normale Canvas nicht zu stören
|
||||
if (data.width > 16 && data.height > 16) {
|
||||
const pixelCount = data.width * data.height * 4; // RGBA-Kanäle
|
||||
|
||||
// Präzise, aber wenig wahrnehmbare Änderungen
|
||||
const minPixels = Math.max(10, Math.floor(pixelCount * 0.005)); // Mindestens 10 Pixel
|
||||
const maxPixels = Math.min(100, Math.floor(pixelCount * 0.01)); // Höchstens 100 Pixel
|
||||
|
||||
// Zufällige Anzahl an Pixeln auswählen
|
||||
const pixelsToModify = Math.floor(Math.random() * (maxPixels - minPixels)) + minPixels;
|
||||
|
||||
// Pixel modifizieren
|
||||
for (let i = 0; i < pixelsToModify; i++) {
|
||||
// Zufälligen Pixel auswählen
|
||||
const offset = Math.floor(Math.random() * pixelCount / 4) * 4;
|
||||
|
||||
// Nur RGB modifizieren, Alpha-Kanal unverändert lassen
|
||||
for (let j = 0; j < 3; j++) {
|
||||
// Subtile Änderungen hinzufügen (±1-2)
|
||||
const mod = Math.floor(Math.random() * 3) - 1;
|
||||
data.data[offset + j] = Math.max(0, Math.min(255, data.data[offset + j] + mod));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// getImageData überschreiben
|
||||
CanvasRenderingContext2D.prototype.getImageData = function() {
|
||||
const imageData = originalGetImageData.apply(this, arguments);
|
||||
return addNoise(imageData, NOISE_LEVEL);
|
||||
};
|
||||
|
||||
// toDataURL überschreiben
|
||||
HTMLCanvasElement.prototype.toDataURL = function() {
|
||||
// Temporäre Modifikation für getImageData
|
||||
const tempGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
||||
CanvasRenderingContext2D.prototype.getImageData = originalGetImageData;
|
||||
|
||||
// toDataURL aufrufen
|
||||
let dataURL = originalToDataURL.apply(this, arguments);
|
||||
|
||||
// Wenn das Canvas groß genug ist und nicht von kritischen Anwendungen verwendet wird
|
||||
if (this.width > 16 && this.height > 16 && !this.hasAttribute('data-fingerprint-protect-ignore')) {
|
||||
// Rauschen in binären Teil des DataURL einfügen
|
||||
const parts = dataURL.split(',');
|
||||
if (parts.length === 2) {
|
||||
// Binäre Daten dekodieren
|
||||
const binary = atob(parts[1]);
|
||||
// Geringfügige Änderungen bei etwa 0,1% der Bytes
|
||||
let modifiedBinary = '';
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
if (Math.random() < 0.001 * NOISE_LEVEL) {
|
||||
// Byte leicht ändern
|
||||
const byte = binary.charCodeAt(i);
|
||||
const mod = Math.floor(Math.random() * 3) - 1;
|
||||
modifiedBinary += String.fromCharCode(Math.max(0, Math.min(255, byte + mod)));
|
||||
} else {
|
||||
modifiedBinary += binary[i];
|
||||
}
|
||||
}
|
||||
// Binäre Daten wieder kodieren
|
||||
dataURL = parts[0] + ',' + btoa(modifiedBinary);
|
||||
}
|
||||
}
|
||||
|
||||
// getImageData wiederherstellen
|
||||
CanvasRenderingContext2D.prototype.getImageData = tempGetImageData;
|
||||
|
||||
return dataURL;
|
||||
};
|
||||
|
||||
// toBlob überschreiben
|
||||
HTMLCanvasElement.prototype.toBlob = function(callback) {
|
||||
// Original toBlob aufrufen
|
||||
originalToBlob.apply(this, [function(blob) {
|
||||
// Wenn das Canvas groß genug ist und nicht von kritischen Anwendungen verwendet wird
|
||||
if (this.width > 16 && this.height > 16 && !this.hasAttribute('data-fingerprint-protect-ignore')) {
|
||||
// Blob zu ArrayBuffer konvertieren
|
||||
const reader = new FileReader();
|
||||
reader.onload = function() {
|
||||
const arrayBuffer = reader.result;
|
||||
const array = new Uint8Array(arrayBuffer);
|
||||
|
||||
// Geringfügige Änderungen bei etwa 0,1% der Bytes
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (Math.random() < 0.001 * NOISE_LEVEL) {
|
||||
// Byte leicht ändern
|
||||
const mod = Math.floor(Math.random() * 3) - 1;
|
||||
array[i] = Math.max(0, Math.min(255, array[i] + mod));
|
||||
}
|
||||
}
|
||||
|
||||
// Neuen Blob erstellen
|
||||
const modifiedBlob = new Blob([array], {type: blob.type});
|
||||
callback(modifiedBlob);
|
||||
};
|
||||
reader.readAsArrayBuffer(blob);
|
||||
} else {
|
||||
// Unveränderten Blob zurückgeben
|
||||
callback(blob);
|
||||
}
|
||||
}.bind(this)]);
|
||||
};
|
||||
}
|
||||
""".replace("NOISE_LEVEL", str(self.noise_level))
|
||||
|
||||
self.scripts.append(script)
|
||||
|
||||
def _init_webgl_protection(self):
|
||||
"""
|
||||
Initialisiert den Schutz gegen WebGL-Fingerprinting.
|
||||
Dies modifiziert WebGL-spezifische Werte, die für Fingerprinting verwendet werden.
|
||||
"""
|
||||
webgl_vendor = self.defaults["webgl_vendor"]
|
||||
webgl_renderer = self.defaults["webgl_renderer"]
|
||||
|
||||
script = f"""
|
||||
() => {{
|
||||
// WebGL Vendor und Renderer spoofen
|
||||
const getParameterProxies = [
|
||||
WebGLRenderingContext.prototype,
|
||||
WebGL2RenderingContext.prototype
|
||||
];
|
||||
|
||||
getParameterProxies.forEach(contextPrototype => {{
|
||||
if (!contextPrototype) return;
|
||||
|
||||
const originalGetParameter = contextPrototype.getParameter;
|
||||
contextPrototype.getParameter = function(parameter) {{
|
||||
// WebGL Vendor (VENDOR)
|
||||
if (parameter === 0x1F00) {{
|
||||
return "{webgl_vendor}";
|
||||
}}
|
||||
|
||||
// WebGL Renderer (RENDERER)
|
||||
if (parameter === 0x1F01) {{
|
||||
return "{webgl_renderer}";
|
||||
}}
|
||||
|
||||
// Unshaded Language Version (SHADING_LANGUAGE_VERSION)
|
||||
if (parameter === 0x8B8C) {{
|
||||
const originalValue = originalGetParameter.call(this, parameter);
|
||||
|
||||
// Subtile Änderungen an der Version
|
||||
const versionMatch = originalValue.match(/^WebGL GLSL ES ([0-9]\\.[0-9][0-9])/);
|
||||
if (versionMatch) {{
|
||||
return originalValue.replace(versionMatch[1],
|
||||
(parseFloat(versionMatch[1]) + (Math.random() * 0.01 - 0.005)).toFixed(2));
|
||||
}}
|
||||
}}
|
||||
|
||||
// VERSION
|
||||
if (parameter === 0x1F02) {{
|
||||
const originalValue = originalGetParameter.call(this, parameter);
|
||||
|
||||
// Subtile Änderungen an der Version
|
||||
const versionMatch = originalValue.match(/^WebGL ([0-9]\\.[0-9])/);
|
||||
if (versionMatch) {{
|
||||
return originalValue.replace(versionMatch[1],
|
||||
(parseFloat(versionMatch[1]) + (Math.random() * 0.01 - 0.005)).toFixed(1));
|
||||
}}
|
||||
}}
|
||||
|
||||
return originalGetParameter.apply(this, arguments);
|
||||
}};
|
||||
}});
|
||||
|
||||
// WebGL Vertex und Fragment Shader spoofen
|
||||
const shaderSourceProxies = [
|
||||
WebGLRenderingContext.prototype,
|
||||
WebGL2RenderingContext.prototype
|
||||
];
|
||||
|
||||
shaderSourceProxies.forEach(contextPrototype => {{
|
||||
if (!contextPrototype) return;
|
||||
|
||||
const originalShaderSource = contextPrototype.shaderSource;
|
||||
contextPrototype.shaderSource = function(shader, source) {{
|
||||
// Füge geringfügige Unterschiede in Kommentaren ein, ohne die Funktionalität zu beeinträchtigen
|
||||
if (source.indexOf('//') !== -1) {{
|
||||
// Zufälligen Kommentar leicht modifizieren
|
||||
source = source.replace(/\\/\\/(.*?)\\n/g, (match, comment) => {{
|
||||
if (Math.random() < 0.1) {{
|
||||
// Füge ein Leerzeichen oder einen Bindestrich hinzu oder entferne eines
|
||||
const modifications = [
|
||||
' ', '-', '', ' '
|
||||
];
|
||||
const mod = modifications[Math.floor(Math.random() * modifications.length)];
|
||||
return `//` + comment + mod + `\\n`;
|
||||
}}
|
||||
return match;
|
||||
}});
|
||||
}}
|
||||
|
||||
// Ersetze bestimmte Whitespace-Muster
|
||||
source = source.replace(/\\s{2,}/g, match => {{
|
||||
if (Math.random() < 0.05) {{
|
||||
return ' '.repeat(match.length + (Math.random() < 0.5 ? 1 : -1));
|
||||
}}
|
||||
return match;
|
||||
}});
|
||||
|
||||
return originalShaderSource.call(this, shader, source);
|
||||
}};
|
||||
}});
|
||||
|
||||
// Canvas-Kontext spoofen
|
||||
const getContextProxies = [
|
||||
HTMLCanvasElement.prototype
|
||||
];
|
||||
|
||||
getContextProxies.forEach(canvasPrototype => {{
|
||||
const originalGetContext = canvasPrototype.getContext;
|
||||
canvasPrototype.getContext = function(contextType, contextAttributes) {{
|
||||
const context = originalGetContext.apply(this, arguments);
|
||||
|
||||
if (context && (contextType === 'webgl' || contextType === 'experimental-webgl' || contextType === 'webgl2')) {{
|
||||
// Zufällige Werte für verschiedene Parameter einführen
|
||||
if ({str(self.defaults["webgl_noise"]).lower()}) {{
|
||||
// Zufällige Modifikation für MAX_VERTEX_UNIFORM_VECTORS
|
||||
const MAX_VERTEX_UNIFORM_VECTORS = context.getParameter(context.MAX_VERTEX_UNIFORM_VECTORS);
|
||||
Object.defineProperty(context, 'MAX_VERTEX_UNIFORM_VECTORS', {{
|
||||
get: () => MAX_VERTEX_UNIFORM_VECTORS + Math.floor(Math.random() * 3) - 1
|
||||
}});
|
||||
|
||||
// Zufällige Modifikation für MAX_FRAGMENT_UNIFORM_VECTORS
|
||||
const MAX_FRAGMENT_UNIFORM_VECTORS = context.getParameter(context.MAX_FRAGMENT_UNIFORM_VECTORS);
|
||||
Object.defineProperty(context, 'MAX_FRAGMENT_UNIFORM_VECTORS', {{
|
||||
get: () => MAX_FRAGMENT_UNIFORM_VECTORS + Math.floor(Math.random() * 3) - 1
|
||||
}});
|
||||
}}
|
||||
}}
|
||||
|
||||
return context;
|
||||
}};
|
||||
}});
|
||||
}}
|
||||
"""
|
||||
|
||||
self.scripts.append(script)
|
||||
|
||||
def _init_audio_protection(self):
|
||||
"""
|
||||
Initialisiert den Schutz gegen Audio-Fingerprinting.
|
||||
Dies modifiziert die Audio-API-Funktionen, die für Fingerprinting verwendet werden.
|
||||
"""
|
||||
script = f"""
|
||||
() => {{
|
||||
// Audio-Kontext spoofen
|
||||
if (window.AudioContext || window.webkitAudioContext) {{
|
||||
const AudioContextProxy = window.AudioContext || window.webkitAudioContext;
|
||||
const originalAudioContext = AudioContextProxy;
|
||||
|
||||
// AudioContext überschreiben
|
||||
window.AudioContext = window.webkitAudioContext = function() {{
|
||||
const context = new originalAudioContext();
|
||||
|
||||
// createOscillator überschreiben
|
||||
const originalCreateOscillator = context.createOscillator;
|
||||
context.createOscillator = function() {{
|
||||
const oscillator = originalCreateOscillator.apply(this, arguments);
|
||||
|
||||
// Frequenz leicht modifizieren
|
||||
const originalFrequency = oscillator.frequency;
|
||||
Object.defineProperty(oscillator, 'frequency', {{
|
||||
get: function() {{
|
||||
return originalFrequency;
|
||||
}},
|
||||
set: function(value) {{
|
||||
if (typeof value === 'number') {{
|
||||
// Leichte Änderung hinzufügen
|
||||
const noise = (Math.random() * 0.02 - 0.01) * value;
|
||||
originalFrequency.value = value + noise;
|
||||
}} else {{
|
||||
originalFrequency.value = value;
|
||||
}}
|
||||
}}
|
||||
}});
|
||||
|
||||
return oscillator;
|
||||
}};
|
||||
|
||||
// getChannelData überschreiben
|
||||
const originalGetChannelData = context.createBuffer.prototype?.getChannelData || OfflineAudioContext.prototype?.getChannelData;
|
||||
if (originalGetChannelData) {{
|
||||
context.createBuffer.prototype.getChannelData = function(channel) {{
|
||||
const array = originalGetChannelData.call(this, channel);
|
||||
|
||||
if ({str(self.defaults["audio_noise"]).lower()} && this.length > 20) {{
|
||||
// Sehr subtiles Rauschen hinzufügen (bei etwa 0,1% der Samples)
|
||||
const noise = {self.noise_level} * 0.0001;
|
||||
|
||||
// Effiziente Implementierung
|
||||
const samples = Math.min(200, Math.floor(array.length * 0.001));
|
||||
for (let i = 0; i < samples; i++) {{
|
||||
const idx = Math.floor(Math.random() * array.length);
|
||||
array[idx] += (Math.random() * 2 - 1) * noise;
|
||||
}}
|
||||
}}
|
||||
|
||||
return array;
|
||||
}};
|
||||
}}
|
||||
|
||||
// AnalyserNode.getFloatFrequencyData überschreiben
|
||||
if (context.createAnalyser) {{
|
||||
const originalCreateAnalyser = context.createAnalyser;
|
||||
context.createAnalyser = function() {{
|
||||
const analyser = originalCreateAnalyser.apply(this, arguments);
|
||||
const originalGetFloatFrequencyData = analyser.getFloatFrequencyData;
|
||||
|
||||
analyser.getFloatFrequencyData = function(array) {{
|
||||
originalGetFloatFrequencyData.call(this, array);
|
||||
|
||||
if ({str(self.defaults["audio_noise"]).lower()} && array.length > 20) {{
|
||||
// Sehr subtiles Rauschen hinzufügen
|
||||
const noise = {self.noise_level} * 0.001;
|
||||
|
||||
// Effiziente Implementierung für große Arrays
|
||||
const samples = Math.min(20, Math.floor(array.length * 0.01));
|
||||
for (let i = 0; i < samples; i++) {{
|
||||
const idx = Math.floor(Math.random() * array.length);
|
||||
array[idx] += (Math.random() * 2 - 1) * noise;
|
||||
}}
|
||||
}}
|
||||
|
||||
return array;
|
||||
}};
|
||||
|
||||
return analyser;
|
||||
}};
|
||||
}}
|
||||
|
||||
return context;
|
||||
}};
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
self.scripts.append(script)
|
||||
|
||||
def _init_navigator_protection(self):
|
||||
"""
|
||||
Initialisiert den Schutz gegen Navigator-Objekt-Fingerprinting.
|
||||
Dies modifiziert verschiedene Navigator-Eigenschaften, die für Fingerprinting verwendet werden.
|
||||
"""
|
||||
hardware_concurrency = self.defaults["hardware_concurrency"]
|
||||
device_memory = self.defaults["device_memory"]
|
||||
|
||||
script = f"""
|
||||
() => {{
|
||||
// Navigator-Eigenschaften überschreiben
|
||||
|
||||
// hardwareConcurrency (CPU-Kerne)
|
||||
if (navigator.hardwareConcurrency) {{
|
||||
Object.defineProperty(navigator, 'hardwareConcurrency', {{
|
||||
get: () => {hardware_concurrency}
|
||||
}});
|
||||
}}
|
||||
|
||||
// deviceMemory (RAM)
|
||||
if (navigator.deviceMemory) {{
|
||||
Object.defineProperty(navigator, 'deviceMemory', {{
|
||||
get: () => {device_memory}
|
||||
}});
|
||||
}}
|
||||
|
||||
// language und languages
|
||||
if (navigator.language) {{
|
||||
const originalLanguage = navigator.language;
|
||||
const originalLanguages = navigator.languages;
|
||||
|
||||
Object.defineProperty(navigator, 'language', {{
|
||||
get: () => originalLanguage
|
||||
}});
|
||||
|
||||
Object.defineProperty(navigator, 'languages', {{
|
||||
get: () => originalLanguages
|
||||
}});
|
||||
}}
|
||||
|
||||
// userAgent-Konsistenz sicherstellen
|
||||
if (navigator.userAgent) {{
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
// Wenn der userAgent bereits überschrieben wurde, stellen wir
|
||||
// sicher, dass die appVersion und platform konsistent sind
|
||||
const browserInfo = {{
|
||||
chrome: /Chrome\\/(\\d+)/.exec(userAgent),
|
||||
firefox: /Firefox\\/(\\d+)/.exec(userAgent),
|
||||
safari: /Safari\\/(\\d+)/.exec(userAgent),
|
||||
edge: /Edg(e|)\\/(\\d+)/.exec(userAgent)
|
||||
}};
|
||||
|
||||
// Platform basierend auf userAgent bestimmen
|
||||
let platform = '{self.defaults.get("platform", "Win32")}';
|
||||
if (/Windows/.test(userAgent)) platform = 'Win32';
|
||||
else if (/Macintosh/.test(userAgent)) platform = 'MacIntel';
|
||||
else if (/Linux/.test(userAgent)) platform = 'Linux x86_64';
|
||||
else if (/Android/.test(userAgent)) platform = 'Linux armv8l';
|
||||
else if (/iPhone|iPad/.test(userAgent)) platform = 'iPhone';
|
||||
|
||||
Object.defineProperty(navigator, 'platform', {{
|
||||
get: () => platform
|
||||
}});
|
||||
|
||||
// appVersion konsistent machen
|
||||
if (navigator.appVersion) {{
|
||||
Object.defineProperty(navigator, 'appVersion', {{
|
||||
get: () => userAgent.substring(8)
|
||||
}});
|
||||
}}
|
||||
|
||||
// vendor basierend auf Browser setzen
|
||||
let vendor = '{self.defaults.get("vendor", "Google Inc.")}';
|
||||
if (browserInfo.safari) vendor = 'Apple Computer, Inc.';
|
||||
else if (browserInfo.firefox) vendor = '';
|
||||
|
||||
Object.defineProperty(navigator, 'vendor', {{
|
||||
get: () => vendor
|
||||
}});
|
||||
}}
|
||||
|
||||
"""
|
||||
self.scripts.append(script)
|
||||
|
||||
def _init_misc_protections(self):
|
||||
"""
|
||||
Initialisiert verschiedene weitere Schutzmaßnahmen gegen Fingerprinting.
|
||||
"""
|
||||
timezone_id = self.defaults["timezone_id"]
|
||||
|
||||
script = f"""
|
||||
() => {{
|
||||
// Date.prototype.getTimezoneOffset überschreiben
|
||||
const originalGetTimezoneOffset = Date.prototype.getTimezoneOffset;
|
||||
Date.prototype.getTimezoneOffset = function() {{
|
||||
// Zeitzonendaten für '{timezone_id}'
|
||||
// Mitteleuropäische Zeit (CET/CEST): UTC+1 / UTC+2 (Sommerzeit)
|
||||
const date = new Date(this);
|
||||
|
||||
// Prüfen, ob Sommerzeit
|
||||
const jan = new Date(date.getFullYear(), 0, 1);
|
||||
const jul = new Date(date.getFullYear(), 6, 1);
|
||||
const standardOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
|
||||
|
||||
// Sommerzeit in Europa: Ende März bis Ende Oktober
|
||||
const isDST = date.getMonth() > 2 && date.getMonth() < 10;
|
||||
|
||||
// Offset in Minuten: CET = -60, CEST = -120
|
||||
return isDST ? -120 : -60;
|
||||
}};
|
||||
|
||||
// Plugins und MimeTypes Schutz
|
||||
if (navigator.plugins) {{
|
||||
// Leere oder gefälschte Plugins
|
||||
Object.defineProperty(navigator, 'plugins', {{
|
||||
get: () => {{
|
||||
// Plugins-Array-Eigenschaften simulieren
|
||||
const plugins = {{
|
||||
length: 0,
|
||||
item: () => null,
|
||||
namedItem: () => null,
|
||||
refresh: () => {{}},
|
||||
[Symbol.iterator]: function* () {{}}
|
||||
}};
|
||||
|
||||
return plugins;
|
||||
}}
|
||||
}});
|
||||
|
||||
// MimeTypes ebenfalls leeren
|
||||
if (navigator.mimeTypes) {{
|
||||
Object.defineProperty(navigator, 'mimeTypes', {{
|
||||
get: () => {{
|
||||
// MimeTypes-Array-Eigenschaften simulieren
|
||||
const mimeTypes = {{
|
||||
length: 0,
|
||||
item: () => null,
|
||||
namedItem: () => null,
|
||||
[Symbol.iterator]: function* () {{}}
|
||||
}};
|
||||
|
||||
return mimeTypes;
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
}}
|
||||
|
||||
// Performance.now() und Date.now() - Schutz gegen Timing-Angriffe
|
||||
if (window.performance && performance.now) {{
|
||||
const originalNow = performance.now;
|
||||
|
||||
performance.now = function() {{
|
||||
const value = originalNow.call(performance);
|
||||
// Subtile Abweichung hinzufügen
|
||||
return value + (Math.random() * 0.01);
|
||||
}};
|
||||
}}
|
||||
|
||||
// Date.now() ebenfalls mit subtiler Abweichung
|
||||
const originalDateNow = Date.now;
|
||||
Date.now = function() {{
|
||||
const value = originalDateNow.call(Date);
|
||||
// Subtile Abweichung hinzufügen (±1ms)
|
||||
return value + (Math.random() < 0.5 ? 1 : 0);
|
||||
}};
|
||||
|
||||
// screen-Eigenschaften konsistent machen
|
||||
if (window.screen) {{
|
||||
const originalWidth = screen.width;
|
||||
const originalHeight = screen.height;
|
||||
const originalColorDepth = screen.colorDepth;
|
||||
const originalPixelDepth = screen.pixelDepth;
|
||||
|
||||
// Abweichungen verhindern - konsistente Werte liefern
|
||||
Object.defineProperties(screen, {{
|
||||
'width': {{ get: () => originalWidth }},
|
||||
'height': {{ get: () => originalHeight }},
|
||||
'availWidth': {{ get: () => originalWidth }},
|
||||
'availHeight': {{ get: () => originalHeight - 40 }}, // Taskleiste simulieren
|
||||
'colorDepth': {{ get: () => originalColorDepth }},
|
||||
'pixelDepth': {{ get: () => originalPixelDepth }}
|
||||
}});
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
self.scripts.append(script)
|
||||
|
||||
def apply_to_context(self, context=None):
|
||||
"""
|
||||
Wendet alle Skripte auf den Browser-Kontext an.
|
||||
|
||||
Args:
|
||||
context: Der Browser-Kontext, falls er noch nicht gesetzt wurde
|
||||
"""
|
||||
if context:
|
||||
self.context = context
|
||||
|
||||
if not self.context:
|
||||
logger.warning("Kein Browser-Kontext zum Anwenden der Fingerprint-Schutzmaßnahmen")
|
||||
return
|
||||
|
||||
for script in self.scripts:
|
||||
self.context.add_init_script(script)
|
||||
|
||||
logger.info(f"Fingerprint-Schutzmaßnahmen auf Browser-Kontext angewendet ({len(self.scripts)} Skripte)")
|
||||
|
||||
def get_fingerprint_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt den aktuellen Status der Fingerprint-Schutzmaßnahmen zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Status der Fingerprint-Schutzmaßnahmen
|
||||
"""
|
||||
status = {
|
||||
"active": self.context is not None,
|
||||
"script_count": len(self.scripts),
|
||||
"protections": {
|
||||
"canvas": self.defaults["canvas_noise"],
|
||||
"webgl": self.defaults["webgl_noise"],
|
||||
"audio": self.defaults["audio_noise"],
|
||||
"navigator": True,
|
||||
"battery": "getBattery" in self.scripts[-1],
|
||||
"timing": "performance.now" in self.scripts[-1]
|
||||
},
|
||||
"noise_level": self.noise_level,
|
||||
"custom_values": {
|
||||
"webgl_vendor": self.defaults["webgl_vendor"],
|
||||
"webgl_renderer": self.defaults["webgl_renderer"],
|
||||
"hardware_concurrency": self.defaults["hardware_concurrency"],
|
||||
"device_memory": self.defaults["device_memory"],
|
||||
"timezone_id": self.defaults["timezone_id"]
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
|
||||
def rotate_fingerprint(self, noise_level: Optional[float] = None):
|
||||
"""
|
||||
Rotiert den Fingerprint durch Neugenerierung der Schutzmaßnahmen.
|
||||
|
||||
Args:
|
||||
noise_level: Optionales neues Rauschniveau (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if noise_level is not None:
|
||||
self.noise_level = max(0.0, min(1.0, noise_level))
|
||||
|
||||
try:
|
||||
# Skripte zurücksetzen
|
||||
self.scripts = []
|
||||
|
||||
# Neues WebGL-Vendor/Renderer-Paar generieren
|
||||
webgl_vendors = [
|
||||
"Google Inc.",
|
||||
"Google Inc. (Intel)",
|
||||
"Google Inc. (NVIDIA)",
|
||||
"Google Inc. (AMD)",
|
||||
"Intel Inc.",
|
||||
"NVIDIA Corporation",
|
||||
"AMD"
|
||||
]
|
||||
|
||||
webgl_renderers = [
|
||||
"ANGLE (Intel, Intel(R) HD Graphics 620 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"ANGLE (AMD, AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"Intel Iris OpenGL Engine",
|
||||
"NVIDIA GeForce GTX 980 OpenGL Engine",
|
||||
"AMD Radeon Pro 560 OpenGL Engine",
|
||||
"Mesa DRI Intel(R) UHD Graphics 620 (KBL GT2)",
|
||||
"Mesa DRI NVIDIA GeForce GTX 1650"
|
||||
]
|
||||
|
||||
# Zufällige Werte wählen
|
||||
self.defaults["webgl_vendor"] = random.choice(webgl_vendors)
|
||||
self.defaults["webgl_renderer"] = random.choice(webgl_renderers)
|
||||
|
||||
# Hardware-Concurrency und Device-Memory variieren
|
||||
self.defaults["hardware_concurrency"] = random.choice([2, 4, 6, 8, 12, 16])
|
||||
self.defaults["device_memory"] = random.choice([2, 4, 8, 16])
|
||||
|
||||
# Schutzmaßnahmen neu initialisieren
|
||||
self._init_protections()
|
||||
|
||||
# Auf Kontext anwenden, falls vorhanden
|
||||
if self.context:
|
||||
self.apply_to_context()
|
||||
|
||||
logger.info(f"Fingerprint erfolgreich rotiert (Noise-Level: {self.noise_level:.2f})")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Rotation des Fingerprints: {e}")
|
||||
return False
|
||||
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
|
||||
517
browser/playwright_manager.py
Normale Datei
517
browser/playwright_manager.py
Normale Datei
@ -0,0 +1,517 @@
|
||||
"""
|
||||
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
|
||||
|
||||
# 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):
|
||||
"""
|
||||
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)
|
||||
"""
|
||||
self.headless = headless
|
||||
self.proxy = proxy
|
||||
self.browser_type = browser_type
|
||||
self.user_agent = user_agent
|
||||
self.screenshots_dir = screenshots_dir
|
||||
self.slowmo = slowmo
|
||||
|
||||
# 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()
|
||||
|
||||
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 starten
|
||||
self.browser = browser_instance.launch(
|
||||
headless=self.headless,
|
||||
args=browser_args,
|
||||
slow_mo=self.slowmo
|
||||
)
|
||||
|
||||
# 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.
|
||||
|
||||
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}")
|
||||
key = f"click_{selector}"
|
||||
return self._retry_action(key, lambda: self.click_element(selector, force, timeout))
|
||||
|
||||
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 close(self):
|
||||
"""Schließt den Browser und gibt Ressourcen frei."""
|
||||
try:
|
||||
if self.page:
|
||||
self.page.close()
|
||||
self.page = None
|
||||
|
||||
if self.context:
|
||||
self.context.close()
|
||||
self.context = None
|
||||
|
||||
if self.browser:
|
||||
self.browser.close()
|
||||
self.browser = None
|
||||
|
||||
if self.playwright:
|
||||
self.playwright.stop()
|
||||
self.playwright = None
|
||||
|
||||
logger.info("Browser-Sitzung geschlossen")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Schließen des Browsers: {e}")
|
||||
|
||||
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))
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren