""" 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)