481 Zeilen
16 KiB
Python
481 Zeilen
16 KiB
Python
"""
|
|
Browser Injection Service - Handles fingerprint injection into browser contexts.
|
|
"""
|
|
|
|
import json
|
|
import base64
|
|
from typing import Dict, Any, Optional
|
|
|
|
from domain.entities.browser_fingerprint import BrowserFingerprint
|
|
|
|
|
|
class BrowserInjectionService:
|
|
"""Service for injecting fingerprints into browser contexts."""
|
|
|
|
def generate_fingerprint_scripts(self, fingerprint: BrowserFingerprint) -> Dict[str, str]:
|
|
"""Generate all fingerprint injection scripts."""
|
|
return {
|
|
"canvas_protection": self._generate_canvas_script(fingerprint),
|
|
"webgl_protection": self._generate_webgl_script(fingerprint),
|
|
"webrtc_protection": self._generate_webrtc_script(fingerprint),
|
|
"navigator_override": self._generate_navigator_script(fingerprint),
|
|
"hardware_override": self._generate_hardware_script(fingerprint),
|
|
"timezone_override": self._generate_timezone_script(fingerprint),
|
|
"audio_protection": self._generate_audio_script(fingerprint),
|
|
"font_detection": self._generate_font_script(fingerprint),
|
|
"plugin_override": self._generate_plugin_script(fingerprint)
|
|
}
|
|
|
|
def _generate_canvas_script(self, fingerprint: BrowserFingerprint) -> str:
|
|
"""Generate canvas fingerprint protection script."""
|
|
return f'''
|
|
(function() {{
|
|
const seed = {fingerprint.canvas_noise.seed};
|
|
const noiseLevel = {fingerprint.canvas_noise.noise_level};
|
|
const algorithm = "{fingerprint.canvas_noise.algorithm}";
|
|
|
|
// Deterministic random based on seed
|
|
let randomSeed = seed;
|
|
function seededRandom() {{
|
|
randomSeed = (randomSeed * 9301 + 49297) % 233280;
|
|
return randomSeed / 233280;
|
|
}}
|
|
|
|
// Override toDataURL
|
|
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
HTMLCanvasElement.prototype.toDataURL = function(...args) {{
|
|
const context = this.getContext('2d');
|
|
if (context) {{
|
|
const imageData = context.getImageData(0, 0, this.width, this.height);
|
|
const data = imageData.data;
|
|
|
|
// Apply noise based on algorithm
|
|
for (let i = 0; i < data.length; i += 4) {{
|
|
if (algorithm === 'gaussian') {{
|
|
// Gaussian noise
|
|
const noise = (seededRandom() - 0.5) * 2 * noiseLevel * 255;
|
|
data[i] = Math.max(0, Math.min(255, data[i] + noise));
|
|
data[i+1] = Math.max(0, Math.min(255, data[i+1] + noise));
|
|
data[i+2] = Math.max(0, Math.min(255, data[i+2] + noise));
|
|
}} else if (algorithm === 'uniform') {{
|
|
// Uniform noise
|
|
const noise = (seededRandom() - 0.5) * noiseLevel * 255;
|
|
data[i] = Math.max(0, Math.min(255, data[i] + noise));
|
|
}}
|
|
}}
|
|
|
|
context.putImageData(imageData, 0, 0);
|
|
}}
|
|
|
|
return originalToDataURL.apply(this, args);
|
|
}};
|
|
|
|
// Override getImageData
|
|
const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
|
CanvasRenderingContext2D.prototype.getImageData = function(...args) {{
|
|
const imageData = originalGetImageData.apply(this, args);
|
|
const data = imageData.data;
|
|
|
|
// Apply same noise
|
|
for (let i = 0; i < data.length; i += 4) {{
|
|
const noise = (seededRandom() - 0.5) * noiseLevel * 255;
|
|
data[i] = Math.max(0, Math.min(255, data[i] + noise));
|
|
}}
|
|
|
|
return imageData;
|
|
}};
|
|
}})();
|
|
'''
|
|
|
|
def _generate_webgl_script(self, fingerprint: BrowserFingerprint) -> str:
|
|
"""Generate WebGL fingerprint override script."""
|
|
return f'''
|
|
(function() {{
|
|
const overrides = {{
|
|
vendor: "{fingerprint.webgl_vendor}",
|
|
renderer: "{fingerprint.webgl_renderer}"
|
|
}};
|
|
|
|
// Override WebGL getParameter
|
|
const getParameter = WebGLRenderingContext.prototype.getParameter;
|
|
WebGLRenderingContext.prototype.getParameter = function(parameter) {{
|
|
if (parameter === 37445) {{ // UNMASKED_VENDOR_WEBGL
|
|
return overrides.vendor;
|
|
}}
|
|
if (parameter === 37446) {{ // UNMASKED_RENDERER_WEBGL
|
|
return overrides.renderer;
|
|
}}
|
|
return getParameter.apply(this, arguments);
|
|
}};
|
|
|
|
// Same for WebGL2
|
|
if (typeof WebGL2RenderingContext !== 'undefined') {{
|
|
const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
|
|
WebGL2RenderingContext.prototype.getParameter = function(parameter) {{
|
|
if (parameter === 37445) return overrides.vendor;
|
|
if (parameter === 37446) return overrides.renderer;
|
|
return getParameter2.apply(this, arguments);
|
|
}};
|
|
}}
|
|
}})();
|
|
'''
|
|
|
|
def _generate_webrtc_script(self, fingerprint: BrowserFingerprint) -> str:
|
|
"""Generate WebRTC protection script."""
|
|
if fingerprint.webrtc_config.disable_webrtc:
|
|
return '''
|
|
(function() {
|
|
// Completely disable WebRTC
|
|
window.RTCPeerConnection = undefined;
|
|
window.RTCSessionDescription = undefined;
|
|
window.RTCIceCandidate = undefined;
|
|
window.webkitRTCPeerConnection = undefined;
|
|
window.mozRTCPeerConnection = undefined;
|
|
})();
|
|
'''
|
|
else:
|
|
return f'''
|
|
(function() {{
|
|
const localIPMask = "{fingerprint.webrtc_config.local_ip_mask}";
|
|
|
|
// Override RTCPeerConnection
|
|
const OriginalRTCPeerConnection = window.RTCPeerConnection ||
|
|
window.webkitRTCPeerConnection ||
|
|
window.mozRTCPeerConnection;
|
|
|
|
if (OriginalRTCPeerConnection) {{
|
|
window.RTCPeerConnection = function(config, constraints) {{
|
|
const pc = new OriginalRTCPeerConnection(config, constraints);
|
|
|
|
// Override createDataChannel to prevent IP leak
|
|
const originalCreateDataChannel = pc.createDataChannel;
|
|
pc.createDataChannel = function(...args) {{
|
|
return originalCreateDataChannel.apply(pc, args);
|
|
}};
|
|
|
|
// Monitor ICE candidates
|
|
pc.addEventListener('icecandidate', function(event) {{
|
|
if (event.candidate && event.candidate.candidate) {{
|
|
// Mask local IP addresses
|
|
event.candidate.candidate = event.candidate.candidate.replace(
|
|
/([0-9]{{1,3}}\.){{3}}[0-9]{{1,3}}/g,
|
|
function(match) {{
|
|
if (match.startsWith('10.') ||
|
|
match.startsWith('192.168.') ||
|
|
match.startsWith('172.')) {{
|
|
return localIPMask;
|
|
}}
|
|
return match;
|
|
}}
|
|
);
|
|
}}
|
|
}});
|
|
|
|
return pc;
|
|
}};
|
|
|
|
// Copy static properties
|
|
Object.keys(OriginalRTCPeerConnection).forEach(key => {{
|
|
window.RTCPeerConnection[key] = OriginalRTCPeerConnection[key];
|
|
}});
|
|
}}
|
|
}})();
|
|
'''
|
|
|
|
def _generate_navigator_script(self, fingerprint: BrowserFingerprint) -> str:
|
|
"""Generate navigator properties override script."""
|
|
nav = fingerprint.navigator_props
|
|
languages_json = json.dumps(nav.languages) if nav.languages else '["en-US", "en"]'
|
|
|
|
return f'''
|
|
(function() {{
|
|
// Navigator overrides
|
|
Object.defineProperty(navigator, 'platform', {{
|
|
get: () => "{nav.platform}"
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'vendor', {{
|
|
get: () => "{nav.vendor}"
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'vendorSub', {{
|
|
get: () => "{nav.vendor_sub}"
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'product', {{
|
|
get: () => "{nav.product}"
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'productSub', {{
|
|
get: () => "{nav.product_sub}"
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'appName', {{
|
|
get: () => "{nav.app_name}"
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'appVersion', {{
|
|
get: () => "{nav.app_version}"
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'userAgent', {{
|
|
get: () => "{nav.user_agent}"
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'language', {{
|
|
get: () => "{nav.language}"
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'languages', {{
|
|
get: () => {languages_json}
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'onLine', {{
|
|
get: () => {str(nav.online).lower()}
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'doNotTrack', {{
|
|
get: () => "{nav.do_not_track}"
|
|
}});
|
|
}})();
|
|
'''
|
|
|
|
def _generate_hardware_script(self, fingerprint: BrowserFingerprint) -> str:
|
|
"""Generate hardware properties override script."""
|
|
hw = fingerprint.hardware_config
|
|
|
|
return f'''
|
|
(function() {{
|
|
// Hardware overrides
|
|
Object.defineProperty(navigator, 'hardwareConcurrency', {{
|
|
get: () => {hw.hardware_concurrency}
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'deviceMemory', {{
|
|
get: () => {hw.device_memory}
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'maxTouchPoints', {{
|
|
get: () => {hw.max_touch_points}
|
|
}});
|
|
|
|
// Screen overrides
|
|
Object.defineProperty(screen, 'width', {{
|
|
get: () => {hw.screen_resolution[0]}
|
|
}});
|
|
|
|
Object.defineProperty(screen, 'height', {{
|
|
get: () => {hw.screen_resolution[1]}
|
|
}});
|
|
|
|
Object.defineProperty(screen, 'availWidth', {{
|
|
get: () => {hw.screen_resolution[0]}
|
|
}});
|
|
|
|
Object.defineProperty(screen, 'availHeight', {{
|
|
get: () => {hw.screen_resolution[1] - 40} // Taskbar
|
|
}});
|
|
|
|
Object.defineProperty(screen, 'colorDepth', {{
|
|
get: () => {hw.color_depth}
|
|
}});
|
|
|
|
Object.defineProperty(screen, 'pixelDepth', {{
|
|
get: () => {hw.color_depth}
|
|
}});
|
|
|
|
Object.defineProperty(window, 'devicePixelRatio', {{
|
|
get: () => {hw.pixel_ratio}
|
|
}});
|
|
}})();
|
|
'''
|
|
|
|
def _generate_timezone_script(self, fingerprint: BrowserFingerprint) -> str:
|
|
"""Generate timezone override script."""
|
|
return f'''
|
|
(function() {{
|
|
const timezone = "{fingerprint.timezone}";
|
|
const timezoneOffset = {fingerprint.timezone_offset};
|
|
|
|
// Override Date.prototype.getTimezoneOffset
|
|
Date.prototype.getTimezoneOffset = function() {{
|
|
return timezoneOffset;
|
|
}};
|
|
|
|
// Override Intl.DateTimeFormat
|
|
const OriginalDateTimeFormat = Intl.DateTimeFormat;
|
|
Intl.DateTimeFormat = function(...args) {{
|
|
if (args.length === 0 || !args[1] || !args[1].timeZone) {{
|
|
if (!args[1]) args[1] = {{}};
|
|
args[1].timeZone = timezone;
|
|
}}
|
|
return new OriginalDateTimeFormat(...args);
|
|
}};
|
|
|
|
// Copy static methods
|
|
Object.keys(OriginalDateTimeFormat).forEach(key => {{
|
|
Intl.DateTimeFormat[key] = OriginalDateTimeFormat[key];
|
|
}});
|
|
|
|
// Override resolvedOptions
|
|
Intl.DateTimeFormat.prototype.resolvedOptions = function() {{
|
|
const options = OriginalDateTimeFormat.prototype.resolvedOptions.call(this);
|
|
options.timeZone = timezone;
|
|
return options;
|
|
}};
|
|
}})();
|
|
'''
|
|
|
|
def _generate_audio_script(self, fingerprint: BrowserFingerprint) -> str:
|
|
"""Generate audio context override script."""
|
|
return f'''
|
|
(function() {{
|
|
const audioParams = {{
|
|
baseLatency: {fingerprint.audio_context_base_latency},
|
|
outputLatency: {fingerprint.audio_context_output_latency},
|
|
sampleRate: {fingerprint.audio_context_sample_rate}
|
|
}};
|
|
|
|
// Override AudioContext
|
|
const OriginalAudioContext = window.AudioContext || window.webkitAudioContext;
|
|
|
|
if (OriginalAudioContext) {{
|
|
window.AudioContext = function(...args) {{
|
|
const context = new OriginalAudioContext(...args);
|
|
|
|
Object.defineProperty(context, 'baseLatency', {{
|
|
get: () => audioParams.baseLatency
|
|
}});
|
|
|
|
Object.defineProperty(context, 'outputLatency', {{
|
|
get: () => audioParams.outputLatency
|
|
}});
|
|
|
|
Object.defineProperty(context, 'sampleRate', {{
|
|
get: () => audioParams.sampleRate
|
|
}});
|
|
|
|
return context;
|
|
}};
|
|
|
|
// Copy static properties
|
|
Object.keys(OriginalAudioContext).forEach(key => {{
|
|
window.AudioContext[key] = OriginalAudioContext[key];
|
|
}});
|
|
}}
|
|
}})();
|
|
'''
|
|
|
|
def _generate_font_script(self, fingerprint: BrowserFingerprint) -> str:
|
|
"""Generate font detection override script."""
|
|
fonts_json = json.dumps(fingerprint.font_list)
|
|
|
|
return f'''
|
|
(function() {{
|
|
const allowedFonts = {fonts_json};
|
|
|
|
// Override font detection methods
|
|
const originalGetComputedStyle = window.getComputedStyle;
|
|
window.getComputedStyle = function(element, pseudoElt) {{
|
|
const style = originalGetComputedStyle.apply(this, arguments);
|
|
const originalPropertyGetter = style.getPropertyValue;
|
|
|
|
style.getPropertyValue = function(prop) {{
|
|
if (prop === 'font-family') {{
|
|
const value = originalPropertyGetter.apply(this, arguments);
|
|
// Filter out non-allowed fonts
|
|
const fonts = value.split(',').map(f => f.trim());
|
|
const filtered = fonts.filter(f => {{
|
|
const fontName = f.replace(/['"]/g, '');
|
|
return allowedFonts.some(allowed =>
|
|
fontName.toLowerCase().includes(allowed.toLowerCase())
|
|
);
|
|
}});
|
|
return filtered.join(', ');
|
|
}}
|
|
return originalPropertyGetter.apply(this, arguments);
|
|
}};
|
|
|
|
return style;
|
|
}};
|
|
}})();
|
|
'''
|
|
|
|
def _generate_plugin_script(self, fingerprint: BrowserFingerprint) -> str:
|
|
"""Generate plugin list override script."""
|
|
plugins_data = []
|
|
for plugin in fingerprint.plugins:
|
|
plugins_data.append({
|
|
"name": plugin.get("name", ""),
|
|
"filename": plugin.get("filename", ""),
|
|
"description": plugin.get("description", ""),
|
|
"version": plugin.get("version", "")
|
|
})
|
|
|
|
plugins_json = json.dumps(plugins_data)
|
|
|
|
return f'''
|
|
(function() {{
|
|
const pluginData = {plugins_json};
|
|
|
|
// Create fake PluginArray
|
|
const fakePlugins = {{}};
|
|
fakePlugins.length = pluginData.length;
|
|
|
|
pluginData.forEach((plugin, index) => {{
|
|
const fakePlugin = {{
|
|
name: plugin.name,
|
|
filename: plugin.filename,
|
|
description: plugin.description,
|
|
version: plugin.version,
|
|
length: 1,
|
|
item: function(index) {{ return this; }},
|
|
namedItem: function(name) {{ return this; }}
|
|
}};
|
|
|
|
fakePlugins[index] = fakePlugin;
|
|
fakePlugins[plugin.name] = fakePlugin;
|
|
}});
|
|
|
|
fakePlugins.item = function(index) {{
|
|
return this[index] || null;
|
|
}};
|
|
|
|
fakePlugins.namedItem = function(name) {{
|
|
return this[name] || null;
|
|
}};
|
|
|
|
fakePlugins.refresh = function() {{}};
|
|
|
|
// Override navigator.plugins
|
|
Object.defineProperty(navigator, 'plugins', {{
|
|
get: () => fakePlugins
|
|
}});
|
|
}})();
|
|
'''
|
|
|
|
def apply_to_browser_context(self, context: Any, fingerprint: BrowserFingerprint) -> None:
|
|
"""Apply fingerprint to a Playwright browser context."""
|
|
# Generate all scripts
|
|
scripts = self.generate_fingerprint_scripts(fingerprint)
|
|
|
|
# Combine all scripts
|
|
combined_script = '\n'.join(scripts.values())
|
|
|
|
# Add script to context
|
|
context.add_init_script(combined_script)
|
|
|
|
# Set viewport
|
|
context.set_viewport_size({
|
|
'width': fingerprint.hardware_config.screen_resolution[0],
|
|
'height': fingerprint.hardware_config.screen_resolution[1]
|
|
})
|
|
|
|
# Set locale
|
|
context.set_locale(fingerprint.navigator_props.language)
|
|
|
|
# Set timezone
|
|
context.set_timezone_id(fingerprint.timezone)
|
|
|
|
# Set user agent
|
|
context.set_user_agent(fingerprint.navigator_props.user_agent) |