diff --git a/examples/fingerprint_example.py b/examples/fingerprint_example.py new file mode 100644 index 00000000..0a5dc134 --- /dev/null +++ b/examples/fingerprint_example.py @@ -0,0 +1,420 @@ +""" +Fingerprint Spoofing Feature Examples + +Demonstrates how to use pydoll's fingerprint spoofing features to prevent browser +fingerprint tracking. +""" + +import asyncio +import traceback + +from pydoll.browser.chromium.chrome import Chrome +from pydoll.browser.chromium.edge import Edge +from pydoll.browser.options import ChromiumOptions +from pydoll.fingerprint import FingerprintConfig, FingerprintManager + + +async def basic_example(): + """Basic example: Enable fingerprint spoofing with one click""" + print("=== Basic Example: Enable Fingerprint Spoofing ===") + + # Create Chrome browser with fingerprint spoofing enabled + options = ChromiumOptions() + options.enable_fingerprint_spoofing_mode() + browser = Chrome(options=options) + + async with browser: + # Start browser + tab = await browser.start() + + # Visit fingerprint detection website + await tab.go_to("https://fingerprintjs.github.io/fingerprintjs/") + + # Wait for page to load + await asyncio.sleep(5) + + # Get fingerprint ID + try: + # Wait enough time for fingerprint generation + await asyncio.sleep(3) + + # Try to use a more generic selector or directly execute JavaScript + # to get fingerprint ID + fingerprint_id = await tab.execute_script(""" + // Wait for fingerprint generation to complete + if (window.fingerprintJsResult) { + return window.fingerprintJsResult.visitorId || 'No ID found'; + } else if (document.querySelector(".visitor-id")) { + return document.querySelector(".visitor-id").textContent; + } else if (document.getElementById("fp-result")) { + return document.getElementById("fp-result").textContent; + } else { + // Try to find any element containing a fingerprint ID + const elements = document.querySelectorAll('*'); + for (const el of elements) { + if (el.textContent && el.textContent.match(/[a-f0-9]{32}/)) { + return el.textContent.match(/[a-f0-9]{32}/)[0]; + } + } + return 'Could not find fingerprint ID on page'; + } + """) + print(f"Generated fingerprint ID: {fingerprint_id}") + except Exception as e: + print(f"Failed to get fingerprint ID: {e}") + traceback.print_exc() + + # Take screenshot to save result + try: + await tab.take_screenshot("fingerprint_result.png") + print("Screenshot saved as fingerprint_result.png") + except Exception as e: + print(f"Screenshot failed: {e}") + + await asyncio.sleep(3) + + +async def custom_config_example(): + """Advanced example: Custom fingerprint configuration""" + print("\n=== Advanced Example: Custom Fingerprint Configuration ===") + + # Create custom fingerprint configuration + config = FingerprintConfig( + # Configure browser and OS settings + browser_type="chrome", + preferred_os="windows", + is_mobile=False, + + # Configure fingerprinting protection features + enable_webgl_spoofing=True, + enable_canvas_spoofing=True, + enable_audio_spoofing=True, + enable_webrtc_spoofing=True, + + # Custom settings + preferred_languages=["zh-CN", "zh", "en-US", "en"], + min_screen_width=1920, + max_screen_width=2560, + min_screen_height=1080, + max_screen_height=1440 + ) + + # Create browser instance + options = ChromiumOptions() + options.enable_fingerprint_spoofing_mode(config=config) + browser = Chrome(options=options) + + async with browser: + tab = await browser.start() + + # Visit fingerprint detection website + await tab.go_to("https://fingerprintjs.github.io/fingerprintjs/") + + await asyncio.sleep(5) + + # Get and print custom fingerprint information + try: + user_agent = await tab.execute_script("return navigator.userAgent") + print(f"Custom User Agent: {user_agent}") + + platform = await tab.execute_script("return navigator.platform") + print(f"Custom Platform: {platform}") + + screen_info = await tab.execute_script(""" + return { + width: screen.width, + height: screen.height, + colorDepth: screen.colorDepth + } + """) + print(f"Custom Screen info: {screen_info}") + + # Get fingerprint ID from the site + fingerprint_id = await tab.execute_script(""" + // Wait for fingerprint generation to complete + if (window.fingerprintJsResult) { + return window.fingerprintJsResult.visitorId || 'No ID found'; + } else if (document.querySelector(".visitor-id")) { + return document.querySelector(".visitor-id").textContent; + } else if (document.getElementById("fp-result")) { + return document.getElementById("fp-result").textContent; + } else { + // Try to find any element containing a fingerprint ID + const elements = document.querySelectorAll('*'); + for (const el of elements) { + if (el.textContent && el.textContent.match(/[a-f0-9]{32}/)) { + return el.textContent.match(/[a-f0-9]{32}/)[0]; + } + } + return 'Could not find fingerprint ID on page'; + } + """) + print(f"Custom config fingerprint ID: {fingerprint_id}") + + except Exception as e: + print(f"Failed to get browser information: {e}") + + await asyncio.sleep(3) + + +async def persistent_fingerprint_example(): + """Persistent fingerprint example: Save and reuse fingerprints""" + print("\n=== Persistent Fingerprint Example ===") + + # Create fingerprint manager + fingerprint_manager = FingerprintManager() + + # First use: Generate and save fingerprint + print("First visit: Generate new fingerprint") + + # Generate a new fingerprint + _ = fingerprint_manager.generate_new_fingerprint("chrome") + + # Save the fingerprint with a custom ID + fingerprint_path = fingerprint_manager.save_fingerprint("my_persistent_fingerprint") + print(f"Saved fingerprint to: {fingerprint_path}") + + # Create browser with the generated fingerprint + options1 = ChromiumOptions() + options1.enable_fingerprint_spoofing_mode(config=FingerprintConfig(browser_type="chrome")) + browser1 = Chrome(options=options1) + + async with browser1: + tab = await browser1.start() + await tab.go_to("https://fingerprintjs.github.io/fingerprintjs/") + await asyncio.sleep(5) + + # Get current fingerprint + current_fingerprint = fingerprint_manager.current_fingerprint + if current_fingerprint: + print(f"Current User Agent: {current_fingerprint.user_agent}") + print(f"Current platform: {current_fingerprint.platform}") + + # Get fingerprint ID from the website + try: + await asyncio.sleep(3) + fingerprint_id = await tab.execute_script(""" + // Wait for fingerprint generation to complete + if (window.fingerprintJsResult) { + return window.fingerprintJsResult.visitorId || 'No ID found'; + } else if (document.querySelector(".visitor-id")) { + return document.querySelector(".visitor-id").textContent; + } else if (document.getElementById("fp-result")) { + return document.getElementById("fp-result").textContent; + } else { + // Try to find any element containing a fingerprint ID + const elements = document.querySelectorAll('*'); + for (const el of elements) { + if (el.textContent && el.textContent.match(/[a-f0-9]{32}/)) { + return el.textContent.match(/[a-f0-9]{32}/)[0]; + } + } + return 'Could not find fingerprint ID on page'; + } + """) + print(f"Persistent fingerprint ID: {fingerprint_id}") + except Exception as e: + print(f"Failed to get fingerprint ID: {e}") + + # Second use: Load saved fingerprint + print("\nSecond visit: Use same fingerprint") + + # Load previously saved fingerprint + saved_fingerprint = fingerprint_manager.load_fingerprint("my_persistent_fingerprint") + if saved_fingerprint: + print(f"Loaded User Agent: {saved_fingerprint.user_agent}") + print(f"Loaded platform: {saved_fingerprint.platform}") + + # List all saved fingerprints + all_fingerprints = fingerprint_manager.list_saved_fingerprints() + print(f"\nAll saved fingerprints: {list(all_fingerprints)}") + + +async def multiple_browsers_example(): + """Multiple browsers example: Run multiple browsers with different fingerprints + simultaneously""" + print("\n=== Multiple Browsers Example ===") + + # Create fingerprint managers to get fingerprint objects + fingerprint_manager1 = FingerprintManager() + fingerprint_manager2 = FingerprintManager() + + # Generate two different fingerprints + fingerprint1 = fingerprint_manager1.generate_new_fingerprint("chrome") + fingerprint2 = fingerprint_manager2.generate_new_fingerprint("chrome") + + # Compare the two fingerprints + print("\nFingerprint Comparison:") + print(f"Fingerprint 1 ID: {fingerprint1.unique_id}") + print(f"Fingerprint 2 ID: {fingerprint2.unique_id}") + + if fingerprint1.unique_id != fingerprint2.unique_id: + print("✓ Success: The two fingerprints have different unique IDs!") + else: + print("✗ Warning: The two fingerprints have the same unique ID") + + # Compare other key attributes + print("\nKey Attributes Comparison:") + print(f"User Agent 1: {fingerprint1.user_agent}") + print(f"User Agent 2: {fingerprint2.user_agent}") + print(f"Platform 1: {fingerprint1.platform}") + print(f"Platform 2: {fingerprint2.platform}") + print(f"Canvas Fingerprint 1: {fingerprint1.canvas_fingerprint}") + print(f"Canvas Fingerprint 2: {fingerprint2.canvas_fingerprint}") + + # Create two browsers with different fingerprints + # Create Chrome browser instances with fingerprint spoofing enabled + options1 = ChromiumOptions() + options1.enable_fingerprint_spoofing_mode(config=FingerprintConfig(browser_type="chrome")) + browser1 = Chrome(options=options1) + + options2 = ChromiumOptions() + options2.enable_fingerprint_spoofing_mode(config=FingerprintConfig(browser_type="chrome")) + browser2 = Chrome(options=options2) + + async with browser1, browser2: + # Start both browsers + tab1 = await browser1.start() + tab2 = await browser2.start() + + # Both visit the same fingerprint detection website + await tab1.go_to("https://fingerprintjs.github.io/fingerprintjs/") + await tab2.go_to("https://fingerprintjs.github.io/fingerprintjs/") + + await asyncio.sleep(5) + + # Get fingerprint IDs from both browsers + try: + # Wait enough time for fingerprint generation + await asyncio.sleep(3) + + # Use JavaScript to get fingerprint ID + fp_id1 = await tab1.execute_script(""" + // Wait for fingerprint generation to complete + if (window.fingerprintJsResult) { + return window.fingerprintJsResult.visitorId || 'No ID found'; + } else if (document.querySelector(".visitor-id")) { + return document.querySelector(".visitor-id").textContent; + } else if (document.getElementById("fp-result")) { + return document.getElementById("fp-result").textContent; + } else { + // Try to find any element containing a fingerprint ID + const elements = document.querySelectorAll('*'); + for (const el of elements) { + if (el.textContent && el.textContent.match(/[a-f0-9]{32}/)) { + return el.textContent.match(/[a-f0-9]{32}/)[0]; + } + } + return 'Could not find fingerprint ID on page'; + } + """) + + fp_id2 = await tab2.execute_script(""" + // Wait for fingerprint generation to complete + if (window.fingerprintJsResult) { + return window.fingerprintJsResult.visitorId || 'No ID found'; + } else if (document.querySelector(".visitor-id")) { + return document.querySelector(".visitor-id").textContent; + } else if (document.getElementById("fp-result")) { + return document.getElementById("fp-result").textContent; + } else { + // Try to find any element containing a fingerprint ID + const elements = document.querySelectorAll('*'); + for (const el of elements) { + if (el.textContent && el.textContent.match(/[a-f0-9]{32}/)) { + return el.textContent.match(/[a-f0-9]{32}/)[0]; + } + } + return 'Could not find fingerprint ID on page'; + } + """) + + print("\nFingerprints detected by the website:") + print(f"Browser 1 Fingerprint ID: {fp_id1}") + print(f"Browser 2 Fingerprint ID: {fp_id2}") + + if fp_id1 != fp_id2: + print("✓ Success: The two browsers generated different fingerprints!") + else: + print("✗ Warning: The two browsers have the same fingerprint") + except Exception as e: + print(f"Failed to get fingerprint IDs: {e}") + traceback.print_exc() + + await asyncio.sleep(3) + + +async def edge_browser_example(): + """Edge browser example""" + print("\n=== Edge Browser Example ===") + + # Create Edge browser with fingerprint spoofing enabled + options = ChromiumOptions() + options.enable_fingerprint_spoofing_mode() + browser = Edge(options=options) + + async with browser: + tab = await browser.start() + + await tab.go_to("https://fingerprintjs.github.io/fingerprintjs/") + await asyncio.sleep(5) + + # Check browser identification + try: + browser_info = await tab.execute_script(""" + return { + userAgent: navigator.userAgent, + appVersion: navigator.appVersion, + vendor: navigator.vendor + } + """) + + print(f"Edge browser info: {browser_info}") + + # Get fingerprint ID from the site + await asyncio.sleep(3) + fingerprint_id = await tab.execute_script(""" + // Wait for fingerprint generation to complete + if (window.fingerprintJsResult) { + return window.fingerprintJsResult.visitorId || 'No ID found'; + } else if (document.querySelector(".visitor-id")) { + return document.querySelector(".visitor-id").textContent; + } else if (document.getElementById("fp-result")) { + return document.getElementById("fp-result").textContent; + } else { + // Try to find any element containing a fingerprint ID + const elements = document.querySelectorAll('*'); + for (const el of elements) { + if (el.textContent && el.textContent.match(/[a-f0-9]{32}/)) { + return el.textContent.match(/[a-f0-9]{32}/)[0]; + } + } + return 'Could not find fingerprint ID on page'; + } + """) + print(f"Edge fingerprint ID: {fingerprint_id}") + + except Exception as e: + print(f"Failed to get browser information: {e}") + + await asyncio.sleep(3) + + +async def main(): + """Run all examples""" + try: + # Run all fingerprint examples using fingerprintjs.github.io + await basic_example() + await custom_config_example() + await persistent_fingerprint_example() + await multiple_browsers_example() + await edge_browser_example() + + except Exception as e: + print(f"Error running examples: {e}") + traceback.print_exc() + + +if __name__ == "__main__": + # Run examples + asyncio.run(main()) diff --git a/examples/refactored_api_example.py b/examples/refactored_api_example.py new file mode 100644 index 00000000..33995ce1 --- /dev/null +++ b/examples/refactored_api_example.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +""" +Example demonstrating the refactored fingerprint options API. + +This example shows how fingerprint-related options are now centralized +in the ChromiumOptions class, making the API cleaner and more maintainable. +""" + +import asyncio + +from pydoll.browser.chromium.chrome import Chrome +from pydoll.browser.chromium.edge import Edge +from pydoll.browser.options import ChromiumOptions + + +async def demonstrate_new_api(): + """Demonstrate the new centralized options API.""" + + print("=== Refactored API Demo: Centralized Fingerprint Options ===\n") + + # Example 1: Basic Chrome usage without fingerprint spoofing + print("1. Basic Chrome usage (no fingerprint spoofing):") + options_basic = ChromiumOptions() + options_basic.add_argument('--window-size=1024,768') + + async with Chrome(options=options_basic) as chrome: + print(f" Fingerprint spoofing: {chrome.enable_fingerprint_spoofing}") + print(" ✓ Basic Chrome instance created successfully\n") + + # Example 2: Enable fingerprint spoofing with the convenience method + print("2. Chrome with fingerprint spoofing (convenience method):") + options_spoofing = ChromiumOptions() + options_spoofing.enable_fingerprint_spoofing_mode() # Enable with default config + + async with Chrome(options=options_spoofing) as chrome: + print(f" Fingerprint spoofing: {chrome.enable_fingerprint_spoofing}") + print(" ✓ Fingerprint spoofing enabled successfully\n") + + # Example 3: Enable fingerprint spoofing with custom configuration + print("3. Chrome with custom fingerprint configuration:") + options_custom = ChromiumOptions() + custom_config = { + "browser_type": "chrome", + "is_mobile": False, + "preferred_os": "windows", + "preferred_languages": ["zh-CN", "en-US"], + "enable_webgl_spoofing": True, + "enable_canvas_spoofing": True + } + options_custom.enable_fingerprint_spoofing_mode(config=custom_config) + + async with Chrome(options=options_custom) as chrome: + print(f" Fingerprint spoofing: {chrome.enable_fingerprint_spoofing}") + print(f" Custom config applied: {options_custom.fingerprint_config}") + print(" ✓ Custom fingerprint configuration applied\n") + + # Example 4: Using property setters for fine-grained control + print("4. Using property setters for fine-grained control:") + options_properties = ChromiumOptions() + options_properties.enable_fingerprint_spoofing = True + options_properties.fingerprint_config = { + "enable_webgl_spoofing": False, + "include_plugins": False + } + options_properties.add_argument('--disable-blink-features=AutomationControlled') + + async with Chrome(options=options_properties) as chrome: + print(f" Fingerprint spoofing: {chrome.enable_fingerprint_spoofing}") + print(f" Config via property: {options_properties.fingerprint_config}") + print(" ✓ Property setters work perfectly\n") + + # Example 5: Same pattern works for Edge + print("5. Edge browser with fingerprint spoofing:") + options_edge = ChromiumOptions() + options_edge.enable_fingerprint_spoofing_mode() + + async with Edge(options=options_edge) as edge: + print(f" Edge fingerprint spoofing: {edge.enable_fingerprint_spoofing}") + print(" ✓ Edge browser with fingerprint spoofing enabled\n") + + print("=== Key Benefits of the Refactored API ===") + print("✓ Centralized configuration: All fingerprint settings in options") + print("✓ Cleaner constructors: No need for extra fingerprint parameters") + print("✓ Better maintainability: One place to manage fingerprint options") + print("✓ Consistent API: Same pattern for Chrome and Edge") + print("✓ Flexibility: Multiple ways to configure (convenience method + properties)") + + +def demonstrate_usage_patterns(): + """Show different usage patterns for the new API.""" + + print("\n=== Different Usage Patterns ===\n") + + # Pattern 1: Method chaining style + print("Pattern 1: Method chaining style") + options1 = ChromiumOptions() + options1.enable_fingerprint_spoofing_mode({"method": "chaining"}) + options1.add_argument('--no-sandbox') + print(" ✓ Configuration complete\n") + + # Pattern 2: Property assignment style + print("Pattern 2: Property assignment style") + options2 = ChromiumOptions() + options2.enable_fingerprint_spoofing = True + options2.fingerprint_config = {"browser_type": "chrome", "is_mobile": False} + print(" ✓ Configuration complete\n") + + # Pattern 3: Build pattern + print("Pattern 3: Builder pattern") + + def build_options_with_fingerprint(config=None): + options = ChromiumOptions() + options.enable_fingerprint_spoofing_mode(config) + options.add_argument('--disable-web-security') + return options + + _options3 = build_options_with_fingerprint({"builder": "pattern"}) + print(" ✓ Builder pattern complete\n") + + print("All patterns provide the same clean, centralized configuration!") + + +if __name__ == "__main__": + print("Running refactored API demonstration...") + + # Demonstrate the new API patterns + demonstrate_usage_patterns() + + # Run the async demo + asyncio.run(demonstrate_new_api()) + + print("\n🎉 Refactored API demonstration complete!") + print("The fingerprint options are now properly centralized in ChromiumOptions!") diff --git a/pydoll/browser/chromium/base.py b/pydoll/browser/chromium/base.py index 38ea7b9d..ae430e4e 100644 --- a/pydoll/browser/chromium/base.py +++ b/pydoll/browser/chromium/base.py @@ -1,8 +1,9 @@ import asyncio +import logging from abc import ABC, abstractmethod from functools import partial from random import randint -from typing import Any, Callable, Optional, TypeVar +from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar from pydoll.browser.interfaces import BrowserOptionsManager from pydoll.browser.managers import ( @@ -10,10 +11,13 @@ ProxyManager, TempDirectoryManager, ) -from pydoll.browser.tab import Tab + +if TYPE_CHECKING: + from pydoll.browser.tab import Tab from pydoll.commands import ( BrowserCommands, FetchCommands, + PageCommands, RuntimeCommands, StorageCommands, TargetCommands, @@ -49,6 +53,8 @@ T = TypeVar('T') +logger = logging.getLogger(__name__) + class Browser(ABC): # noqa: PLR0904 """ @@ -82,6 +88,12 @@ def __init__( self._temp_directory_manager = TempDirectoryManager() self._connection_handler = ConnectionHandler(self._connection_port) + # Store fingerprint manager reference if available + self.fingerprint_manager = getattr(options_manager, 'fingerprint_manager', None) + self.enable_fingerprint_spoofing = getattr( + options_manager, 'enable_fingerprint_spoofing', False + ) + async def __aenter__(self) -> 'Browser': """Async context manager entry.""" return self @@ -93,7 +105,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): await self._connection_handler.close() - async def start(self, headless: bool = False) -> Tab: + async def start(self, headless: bool = False) -> 'Tab': """ Start browser process and establish CDP connection. @@ -124,8 +136,16 @@ async def start(self, headless: bool = False) -> Tab: await self._verify_browser_running() await self._configure_proxy(proxy_config[0], proxy_config[1]) + # Import at runtime to avoid circular import + from pydoll.browser.tab import Tab # noqa: PLC0415 + valid_tab_id = await self._get_valid_tab_id(await self.get_targets()) - return Tab(self, self._connection_port, valid_tab_id) + tab = Tab(self, self._connection_port, valid_tab_id) + + # Inject fingerprint spoofing JavaScript if enabled + await self._setup_fingerprint_for_tab(tab) + + return tab async def stop(self): """ @@ -190,7 +210,7 @@ async def get_browser_contexts(self) -> list[str]: ) return response['result']['browserContextIds'] - async def new_tab(self, url: str = '', browser_context_id: Optional[str] = None) -> Tab: + async def new_tab(self, url: str = '', browser_context_id: Optional[str] = None) -> 'Tab': """ Create new tab for page interaction. @@ -203,12 +223,20 @@ async def new_tab(self, url: str = '', browser_context_id: Optional[str] = None) """ response: CreateTargetResponse = await self._execute_command( TargetCommands.create_target( + url=url, browser_context_id=browser_context_id, ) ) target_id = response['result']['targetId'] + + # Import at runtime to avoid circular import + from pydoll.browser.tab import Tab # noqa: PLC0415 + tab = Tab(self, self._connection_port, target_id, browser_context_id) - if url: await tab.go_to(url) + + # Inject fingerprint spoofing JavaScript if enabled + await self._setup_fingerprint_for_tab(tab) + return tab async def get_targets(self) -> list[TargetInfo]: @@ -224,7 +252,7 @@ async def get_targets(self) -> list[TargetInfo]: response: GetTargetsResponse = await self._execute_command(TargetCommands.get_targets()) return response['result']['targetInfos'] - async def get_opened_tabs(self) -> list[Tab]: + async def get_opened_tabs(self) -> list['Tab']: """ Get all opened tabs that are not extensions and have the type 'page' @@ -237,6 +265,10 @@ async def get_opened_tabs(self) -> list[Tab]: for target in targets if target['type'] == 'page' and 'extension' not in target['url'] ] + + # Import at runtime to avoid circular import + from pydoll.browser.tab import Tab # noqa: PLC0415 + return [ Tab(self, self._connection_port, target['targetId']) for target in reversed(valid_tab_targets) @@ -299,7 +331,7 @@ async def get_window_id_for_target(self, target_id: str) -> int: ) return response['result']['windowId'] - async def get_window_id_for_tab(self, tab: Tab) -> int: + async def get_window_id_for_tab(self, tab: 'Tab') -> int: """Get window ID for tab (convenience method).""" return await self.get_window_id_for_target(tab._target_id) @@ -585,6 +617,58 @@ def _setup_user_dir(self): temp_dir = self._temp_directory_manager.create_temp_dir() self.options.arguments.append(f'--user-data-dir={temp_dir.name}') + async def _setup_fingerprint_for_tab(self, tab): + """ + Setup fingerprint spoofing for a tab if enabled. + + Args: + tab: The tab to setup fingerprint spoofing for. + """ + if self.enable_fingerprint_spoofing and self.fingerprint_manager: + await self._inject_fingerprint_script(tab) + + async def _inject_fingerprint_script(self, tab): + """ + Inject fingerprint spoofing JavaScript into a tab. + + Args: + tab: The tab to inject the script into. + """ + try: + # Get the JavaScript injection code + assert self.fingerprint_manager is not None + script = self.fingerprint_manager.get_fingerprint_js() + + # Inject the script using Page.addScriptToEvaluateOnNewDocument + # This ensures the script runs before any page scripts + await tab._execute_command( + PageCommands.add_script_to_evaluate_on_new_document(script) + ) + + # Also evaluate immediately for current page if it exists + try: + await tab.execute_script(script) + except (RuntimeError, OSError, ValueError): + # Ignore errors for immediate execution as page might not be ready + pass + + except (RuntimeError, OSError, ValueError, AssertionError) as e: + # Don't let fingerprint injection failures break the browser + logger.warning("Failed to inject fingerprint spoofing script: %s", e) + + def get_fingerprint_summary(self) -> Optional[dict]: + """ + Get a summary of the current fingerprint. + + Returns: + Dictionary with fingerprint information, or None if not enabled. + """ + return ( + self.fingerprint_manager.get_fingerprint_summary() + if self.fingerprint_manager + else None + ) + @abstractmethod def _get_default_binary_location(self) -> str: """Get default browser executable path (implemented by subclasses).""" diff --git a/pydoll/browser/chromium/chrome.py b/pydoll/browser/chromium/chrome.py index b73046ec..78ae4e2a 100644 --- a/pydoll/browser/chromium/chrome.py +++ b/pydoll/browser/chromium/chrome.py @@ -1,5 +1,5 @@ import platform -from typing import Optional +from typing import Optional, cast from pydoll.browser.chromium.base import Browser from pydoll.browser.managers import ChromiumOptionsManager @@ -25,6 +25,11 @@ def __init__( """ options_manager = ChromiumOptionsManager(options) super().__init__(options_manager, connection_port) + # Get fingerprint settings from already initialized options + # Cast to ChromiumOptions to access fingerprint properties + chromium_options = cast(ChromiumOptions, self.options) + self.enable_fingerprint_spoofing = chromium_options.enable_fingerprint_spoofing + self.fingerprint_manager = options_manager.get_fingerprint_manager() @staticmethod def _get_default_binary_location(): diff --git a/pydoll/browser/chromium/edge.py b/pydoll/browser/chromium/edge.py index e020b04b..80327d66 100644 --- a/pydoll/browser/chromium/edge.py +++ b/pydoll/browser/chromium/edge.py @@ -1,9 +1,9 @@ import platform -from typing import Optional +from typing import Optional, cast from pydoll.browser.chromium.base import Browser from pydoll.browser.managers import ChromiumOptionsManager -from pydoll.browser.options import Options +from pydoll.browser.options import ChromiumOptions from pydoll.exceptions import UnsupportedOS from pydoll.utils import validate_browser_paths @@ -13,7 +13,7 @@ class Edge(Browser): def __init__( self, - options: Optional[Options] = None, + options: Optional[ChromiumOptions] = None, connection_port: Optional[int] = None, ): """ @@ -25,6 +25,11 @@ def __init__( """ options_manager = ChromiumOptionsManager(options) super().__init__(options_manager, connection_port) + # Get fingerprint settings from already initialized options + # Cast to ChromiumOptions to access fingerprint properties + chromium_options = cast(ChromiumOptions, self.options) + self.enable_fingerprint_spoofing = chromium_options.enable_fingerprint_spoofing + self.fingerprint_manager = options_manager.get_fingerprint_manager() @staticmethod def _get_default_binary_location(): diff --git a/pydoll/browser/managers/browser_options_manager.py b/pydoll/browser/managers/browser_options_manager.py index 54fda7b5..7cc502cf 100644 --- a/pydoll/browser/managers/browser_options_manager.py +++ b/pydoll/browser/managers/browser_options_manager.py @@ -3,6 +3,8 @@ from pydoll.browser.interfaces import BrowserOptionsManager, Options from pydoll.browser.options import ChromiumOptions from pydoll.exceptions import InvalidOptionsObject +from pydoll.fingerprint.manager import FingerprintManager +from pydoll.fingerprint.models import FingerprintConfig class ChromiumOptionsManager(BrowserOptionsManager): @@ -15,6 +17,7 @@ class ChromiumOptionsManager(BrowserOptionsManager): def __init__(self, options: Optional[Options] = None): self.options = options + self.fingerprint_manager: Optional[FingerprintManager] = None def initialize_options( self, @@ -23,7 +26,7 @@ def initialize_options( Initialize and validate browser options. Creates ChromiumOptions if none provided, validates existing options, - and applies default CDP arguments. + and applies default CDP arguments and fingerprint spoofing if enabled. Returns: Properly configured ChromiumOptions instance. @@ -38,9 +41,66 @@ def initialize_options( raise InvalidOptionsObject(f'Expected ChromiumOptions, got {type(self.options)}') self.add_default_arguments() + + # Initialize fingerprint manager if spoofing is enabled in options + if self.options.enable_fingerprint_spoofing: + # Convert dict config to FingerprintConfig object if needed + config = self.options.fingerprint_config + if isinstance(config, dict): + config = FingerprintConfig.from_dict(config) + + self.fingerprint_manager = FingerprintManager(config) + self._apply_fingerprint_spoofing() + return self.options def add_default_arguments(self): """Add default arguments required for CDP integration.""" - self.options.add_argument('--no-first-run') - self.options.add_argument('--no-default-browser-check') + if self.options is not None: + # Add default arguments only if they don't already exist + if '--no-first-run' not in self.options.arguments: + self.options.add_argument('--no-first-run') + if '--no-default-browser-check' not in self.options.arguments: + self.options.add_argument('--no-default-browser-check') + + def _apply_fingerprint_spoofing(self): + """ + Apply fingerprint spoofing arguments to browser options. + """ + if self.fingerprint_manager is None: + return + + # Detect browser type from binary location or default to chrome + browser_type = self._detect_browser_type() + self.fingerprint_manager.generate_new_fingerprint(browser_type) + + # Get fingerprint arguments + fingerprint_args = self.fingerprint_manager.get_fingerprint_arguments(browser_type) + + # Add fingerprint arguments to options + if self.options is not None: + for arg in fingerprint_args: + if arg not in self.options.arguments: + self.options.add_argument(arg) + + def _detect_browser_type(self) -> str: + """ + Detect browser type from options or configuration. + + Returns: + Browser type string ('chrome' or 'edge'). + """ + if self.options and self.options.binary_location: + binary_path = self.options.binary_location.lower() + if 'edge' in binary_path or 'msedge' in binary_path: + return 'edge' + return 'chrome' # Default to chrome + + def get_fingerprint_manager(self) -> Optional[FingerprintManager]: + """ + Get the fingerprint manager instance. + + Returns: + The fingerprint manager if fingerprint spoofing is enabled, None otherwise. + """ + return self.fingerprint_manager diff --git a/pydoll/browser/options.py b/pydoll/browser/options.py index f572c5d4..62cde928 100644 --- a/pydoll/browser/options.py +++ b/pydoll/browser/options.py @@ -20,6 +20,8 @@ def __init__(self): self._arguments = [] self._binary_location = '' self._start_timeout = 10 + self._enable_fingerprint_spoofing = False + self._fingerprint_config = None @property def arguments(self) -> list[str]: @@ -81,6 +83,46 @@ def start_timeout(self, timeout: int): """ self._start_timeout = timeout + @property + def enable_fingerprint_spoofing(self) -> bool: + """ + Gets whether fingerprint spoofing is enabled. + + Returns: + bool: True if fingerprint spoofing is enabled, False otherwise. + """ + return self._enable_fingerprint_spoofing + + @enable_fingerprint_spoofing.setter + def enable_fingerprint_spoofing(self, enabled: bool): + """ + Sets whether fingerprint spoofing is enabled. + + Args: + enabled (bool): True to enable fingerprint spoofing, False to disable. + """ + self._enable_fingerprint_spoofing = enabled + + @property + def fingerprint_config(self): + """ + Gets the fingerprint configuration. + + Returns: + The fingerprint configuration object or None if not set. + """ + return self._fingerprint_config + + @fingerprint_config.setter + def fingerprint_config(self, config): + """ + Sets the fingerprint configuration. + + Args: + config: The fingerprint configuration object. + """ + self._fingerprint_config = config + def add_argument(self, argument: str): """ Adds a command-line argument to the options. @@ -95,3 +137,14 @@ def add_argument(self, argument: str): self._arguments.append(argument) else: raise ArgumentAlreadyExistsInOptions(f'Argument already exists: {argument}') + + def enable_fingerprint_spoofing_mode(self, config=None): + """ + Enable fingerprint spoofing with optional configuration. + + Args: + config: Optional fingerprint configuration object. + """ + self._enable_fingerprint_spoofing = True + if config is not None: + self._fingerprint_config = config diff --git a/pydoll/browser/tab.py b/pydoll/browser/tab.py index cb4f7d55..7041f207 100644 --- a/pydoll/browser/tab.py +++ b/pydoll/browser/tab.py @@ -668,8 +668,12 @@ async def execute_script( Raises: InvalidScriptWithElement: If script contains 'argument' but no element is provided. """ - if 'argument' in script and element is None: - raise InvalidScriptWithElement('Script contains "argument" but no element was provided') + # Check for scripts that specifically use "argument." (element reference) + # but don't provide element. This avoids false positives with JavaScript "arguments" object + if 'argument.' in script and element is None: + raise InvalidScriptWithElement( + 'Script contains "argument." but no element was provided' + ) if element: return await self._execute_script_with_element(script, element) diff --git a/pydoll/commands/target_commands.py b/pydoll/commands/target_commands.py index eb131250..92629811 100644 --- a/pydoll/commands/target_commands.py +++ b/pydoll/commands/target_commands.py @@ -141,7 +141,7 @@ def create_browser_context( @staticmethod def create_target( # noqa: PLR0913, PLR0917 - url: str = 'about:blank', + url: str, left: Optional[int] = None, top: Optional[int] = None, width: Optional[int] = None, diff --git a/pydoll/constants.py b/pydoll/constants.py index 2654c6f4..f87f6224 100644 --- a/pydoll/constants.py +++ b/pydoll/constants.py @@ -46,12 +46,19 @@ class Scripts: """ CLICK_OPTION_TAG = """ - function() { - this.selected = true; - var select = this.parentElement.closest('select'); + document.querySelector('option[value="{self.value}"]').selected = true; + var selectParentXpath = ( + '//option[@value="{self.value}"]//ancestor::select' + ); + var select = document.evaluate( + selectParentXpath, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ).singleNodeValue; var event = new Event('change', { bubbles: true }); select.dispatchEvent(event); - } """ BOUNDS = """ @@ -119,6 +126,356 @@ class Scripts: } """ + # Fingerprint spoofing related scripts + FINGERPRINT_WRAPPER = """ +(function() {{ + 'use strict'; + + // Disable webdriver detection + Object.defineProperty(navigator, 'webdriver', {{ + get: () => false, + configurable: true + }}); + + // Remove automation indicators + delete window.navigator.webdriver; + delete window.__webdriver_script_fn; + delete window.__webdriver_script_func; + delete window.__webdriver_script_function; + delete window.__fxdriver_evaluate; + delete window.__fxdriver_unwrapped; + delete window._Selenium_IDE_Recorder; + delete window._selenium; + delete window.calledSelenium; + delete window.calledPhantom; + delete window.__nightmare; + delete window._phantom; + delete window.phantom; + delete window.callPhantom; + + {scripts} + + // Override toString to hide modifications + const originalToString = Function.prototype.toString; + Function.prototype.toString = function() {{ + const originalSource = originalToString.call(this); + // Only hide fingerprint-related getter functions that return specific values + if (this.name === 'get' && + originalSource.includes('return') && + (originalSource.includes('false') || + originalSource.includes("'{{") || + originalSource.includes('{{'))) {{ + return 'function get() {{ [native code] }}'; + }} + return originalSource; + }}; +}})(); +""" + + NAVIGATOR_OVERRIDE = """ + // Override navigator properties + Object.defineProperty(navigator, 'userAgent', {{ + get: () => '{user_agent}', + configurable: true + }}); + + Object.defineProperty(navigator, 'platform', {{ + get: () => '{platform}', + configurable: true + }}); + + Object.defineProperty(navigator, 'language', {{ + get: () => '{language}', + configurable: true + }}); + + Object.defineProperty(navigator, 'languages', {{ + get: () => {languages}, + configurable: true + }}); + + Object.defineProperty(navigator, 'hardwareConcurrency', {{ + get: () => {hardware_concurrency}, + configurable: true + }}); + + {device_memory_script} + + Object.defineProperty(navigator, 'cookieEnabled', {{ + get: () => {cookie_enabled}, + configurable: true + }}); + + {do_not_track_script} + """ + + SCREEN_OVERRIDE = """ + // Override screen properties + Object.defineProperty(screen, 'width', {{ + get: () => {screen_width}, + configurable: true + }}); + + Object.defineProperty(screen, 'height', {{ + get: () => {screen_height}, + configurable: true + }}); + + Object.defineProperty(screen, 'colorDepth', {{ + get: () => {screen_color_depth}, + configurable: true + }}); + + Object.defineProperty(screen, 'pixelDepth', {{ + get: () => {screen_pixel_depth}, + configurable: true + }}); + + Object.defineProperty(screen, 'availWidth', {{ + get: () => {available_width}, + configurable: true + }}); + + Object.defineProperty(screen, 'availHeight', {{ + get: () => {available_height}, + configurable: true + }}); + + // Override window dimensions + Object.defineProperty(window, 'innerWidth', {{ + get: () => {inner_width}, + configurable: true + }}); + + Object.defineProperty(window, 'innerHeight', {{ + get: () => {inner_height}, + configurable: true + }}); + + Object.defineProperty(window, 'outerWidth', {{ + get: () => {viewport_width}, + configurable: true + }}); + + Object.defineProperty(window, 'outerHeight', {{ + get: () => {viewport_height}, + configurable: true + }}); + """ + + WEBGL_OVERRIDE = """ + // Override WebGL properties + const getParameter = WebGLRenderingContext.prototype.getParameter; + WebGLRenderingContext.prototype.getParameter = function(parameter) {{ + if (parameter === 37445) {{ // VENDOR + return '{webgl_vendor}'; + }} + if (parameter === 37446) {{ // RENDERER + return '{webgl_renderer}'; + }} + if (parameter === 7938) {{ // VERSION + return '{webgl_version}'; + }} + if (parameter === 35724) {{ // SHADING_LANGUAGE_VERSION + return '{webgl_shading_language_version}'; + }} + return getParameter.call(this, parameter); + }}; + + const getSupportedExtensions = WebGLRenderingContext.prototype.getSupportedExtensions; + WebGLRenderingContext.prototype.getSupportedExtensions = function() {{ + return {webgl_extensions}; + }}; + + // Also override WebGL2 if available + if (window.WebGL2RenderingContext) {{ + const getParameter2 = WebGL2RenderingContext.prototype.getParameter; + WebGL2RenderingContext.prototype.getParameter = function(parameter) {{ + if (parameter === 37445) return '{webgl_vendor}'; + if (parameter === 37446) return '{webgl_renderer}'; + if (parameter === 7938) return '{webgl_version}'; + if (parameter === 35724) return '{webgl_shading_language_version}'; + return getParameter2.call(this, parameter); + }}; + + const getSupportedExtensions2 = WebGL2RenderingContext.prototype.getSupportedExtensions; + WebGL2RenderingContext.prototype.getSupportedExtensions = function() {{ + return {webgl_extensions}; + }}; + }} + """ + + CANVAS_OVERRIDE = """ + // Override canvas fingerprinting + const originalGetContext = HTMLCanvasElement.prototype.getContext; + HTMLCanvasElement.prototype.getContext = function(contextType) {{ + const context = originalGetContext.call(this, contextType); + + if (contextType === '2d') {{ + const originalToDataURL = this.toDataURL; + this.toDataURL = function() {{ + // Generate dynamic base64 with subtle variations + const baseFP = '{canvas_fingerprint}'; + const timeSeed = Date.now() % 10000; + const urlSeed = window.location ? window.location.href.length % 100 : 0; + const variation = (timeSeed + urlSeed).toString(36).slice(-4); + + // Replace some characters in the base fingerprint to create variation + let variedFP = baseFP; + if (baseFP.length > 10) {{ + const replacePos = (timeSeed % (baseFP.length - 8)) + 4; + variedFP = baseFP.substring(0, replacePos) + variation + + baseFP.substring(replacePos + 4); + }} + + return 'data:image/png;base64,' + variedFP; + }}; + + const originalGetImageData = context.getImageData; + context.getImageData = function() {{ + const imageData = originalGetImageData.apply(this, arguments); + + // Create a more robust seed based on context + const seed = (Date.now() + + (window.location ? window.location.href.length : 0) + + imageData.data.length + + (navigator.userAgent ? navigator.userAgent.length : 0)) % 1000000; + + // Simple seeded random function + let seedState = seed; + const seededRandom = () => {{ + seedState = (seedState * 9301 + 49297) % 233280; + return seedState / 233280; + }}; + + // Add context-aware noise to RGB channels + for (let i = 0; i < imageData.data.length; i += 4) {{ + // Use seeded random with position-based variation + const positionSeed = (i / 4) * 0.001; + const noiseIntensity = 3 + (seededRandom() + positionSeed) % 2; // 3-5 range + + // Add small random variations to RGB channels + imageData.data[i] = Math.min(255, Math.max(0, + imageData.data[i] + (seededRandom() - 0.5) * noiseIntensity)); // Red + imageData.data[i + 1] = Math.min(255, Math.max(0, + imageData.data[i + 1] + (seededRandom() - 0.5) * noiseIntensity)); // Green + imageData.data[i + 2] = Math.min(255, Math.max(0, + imageData.data[i + 2] + (seededRandom() - 0.5) * noiseIntensity)); // Blue + // Keep alpha channel unchanged to preserve transparency + }} + return imageData; + }}; + }} + + return context; + }}; + """ + + AUDIO_OVERRIDE = """ + // Override AudioContext properties + if (window.AudioContext || window.webkitAudioContext) {{ + const OriginalAudioContext = window.AudioContext || window.webkitAudioContext; + + function FakeAudioContext() {{ + const context = new OriginalAudioContext(); + + Object.defineProperty(context, 'sampleRate', {{ + get: () => {audio_context_sample_rate}, + configurable: true + }}); + + Object.defineProperty(context, 'state', {{ + get: () => '{audio_context_state}', + configurable: true + }}); + + Object.defineProperty(context.destination, 'maxChannelCount', {{ + get: () => {audio_context_max_channel_count}, + configurable: true + }}); + + return context; + }} + + FakeAudioContext.prototype = OriginalAudioContext.prototype; + window.AudioContext = FakeAudioContext; + if (window.webkitAudioContext) {{ + window.webkitAudioContext = FakeAudioContext; + }} + }} + """ + + PLUGIN_OVERRIDE = """ + // Override plugin information + Object.defineProperty(navigator, 'plugins', {{ + get: () => {{ + const plugins = {{}}; + plugins.length = {plugins_length}; + {plugins_js} + return plugins; + }}, + configurable: true + }}); + """ + + MISC_OVERRIDES = """ + // Override timezone + const originalDateGetTimezoneOffset = Date.prototype.getTimezoneOffset; + Date.prototype.getTimezoneOffset = function() {{ + return {timezone_offset}; + }}; + + // Override Intl.DateTimeFormat + if (window.Intl && window.Intl.DateTimeFormat) {{ + const originalResolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions; + Intl.DateTimeFormat.prototype.resolvedOptions = function() {{ + const options = originalResolvedOptions.call(this); + options.timeZone = '{timezone}'; + return options; + }}; + }} + + // Override connection type + if (navigator.connection || navigator.mozConnection || navigator.webkitConnection) {{ + const connection = navigator.connection || + navigator.mozConnection || + navigator.webkitConnection; + Object.defineProperty(connection, 'effectiveType', {{ + get: () => '{connection_type}', + configurable: true + }}); + }} + + // Hide automation indicators in Chrome + Object.defineProperty(window, 'chrome', {{ + get: () => {{ + return {{ + runtime: {{ + onConnect: undefined, + onMessage: undefined + }} + }}; + }}, + configurable: true + }}); + + // Override permissions + if (navigator.permissions && navigator.permissions.query) {{ + const originalQuery = navigator.permissions.query; + navigator.permissions.query = function(parameters) {{ + return originalQuery(parameters).then(result => {{ + if (parameters.name === 'notifications') {{ + Object.defineProperty(result, 'state', {{ + get: () => 'denied', + configurable: true + }}); + }} + return result; + }}); + }}; + }} + """ + GET_PARENT_NODE = """ function() { return this.parentElement; diff --git a/pydoll/elements/mixins/find_elements_mixin.py b/pydoll/elements/mixins/find_elements_mixin.py index b08b420e..f9d0f846 100644 --- a/pydoll/elements/mixins/find_elements_mixin.py +++ b/pydoll/elements/mixins/find_elements_mixin.py @@ -1,5 +1,5 @@ import asyncio -from typing import TYPE_CHECKING, Literal, Optional, TypeVar, Union, overload +from typing import TYPE_CHECKING, Optional, TypeVar, Union from pydoll.commands import ( DomCommands, @@ -44,76 +44,6 @@ class FindElementsMixin: complex location logic themselves. """ - @overload - async def find( - self, - id: Optional[str] = ..., - class_name: Optional[str] = ..., - name: Optional[str] = ..., - tag_name: Optional[str] = ..., - text: Optional[str] = ..., - timeout: int = ..., - find_all: Literal[False] = False, - raise_exc: Literal[True] = True, - **attributes: dict[str, str], - ) -> 'WebElement': ... - - @overload - async def find( - self, - id: Optional[str] = ..., - class_name: Optional[str] = ..., - name: Optional[str] = ..., - tag_name: Optional[str] = ..., - text: Optional[str] = ..., - timeout: int = ..., - find_all: Literal[True] = True, - raise_exc: Literal[True] = True, - **attributes: dict[str, str], - ) -> list['WebElement']: ... - - @overload - async def find( - self, - id: Optional[str] = ..., - class_name: Optional[str] = ..., - name: Optional[str] = ..., - tag_name: Optional[str] = ..., - text: Optional[str] = ..., - timeout: int = ..., - find_all: Literal[True] = True, - raise_exc: Literal[False] = False, - **attributes: dict[str, str], - ) -> Optional[list['WebElement']]: ... - - @overload - async def find( - self, - id: Optional[str] = ..., - class_name: Optional[str] = ..., - name: Optional[str] = ..., - tag_name: Optional[str] = ..., - text: Optional[str] = ..., - timeout: int = ..., - find_all: Literal[False] = False, - raise_exc: Literal[False] = False, - **attributes: dict[str, str], - ) -> Optional['WebElement']: ... - - @overload - async def find( - self, - id: Optional[str] = ..., - class_name: Optional[str] = ..., - name: Optional[str] = ..., - tag_name: Optional[str] = ..., - text: Optional[str] = ..., - timeout: int = ..., - find_all: bool = ..., - raise_exc: bool = ..., - **attributes: dict[str, str], - ) -> Union['WebElement', list['WebElement'], None]: ... - async def find( # noqa: PLR0913, PLR0917 self, id: Optional[str] = None, @@ -171,51 +101,6 @@ async def find( # noqa: PLR0913, PLR0917 by, value, timeout=timeout, find_all=find_all, raise_exc=raise_exc ) - @overload - async def query( - self, - expression: str, - timeout: int = ..., - find_all: Literal[False] = False, - raise_exc: Literal[True] = True, - ) -> 'WebElement': ... - - @overload - async def query( - self, - expression: str, - timeout: int = ..., - find_all: Literal[False] = False, - raise_exc: Literal[False] = False, - ) -> Optional['WebElement']: ... - - @overload - async def query( - self, - expression: str, - timeout: int = ..., - find_all: Literal[True] = True, - raise_exc: Literal[True] = True, - ) -> list['WebElement']: ... - - @overload - async def query( - self, - expression: str, - timeout: int = ..., - find_all: Literal[True] = True, - raise_exc: Literal[False] = False, - ) -> Optional[list['WebElement']]: ... - - @overload - async def query( - self, - expression: str, - timeout: int = ..., - find_all: bool = ..., - raise_exc: bool = ..., - ) -> Union['WebElement', list['WebElement'], None]: ... - async def query( self, expression: str, timeout: int = 0, find_all: bool = False, raise_exc: bool = True ) -> Union['WebElement', list['WebElement'], None]: diff --git a/pydoll/elements/web_element.py b/pydoll/elements/web_element.py index d070f4ea..ea494eb8 100644 --- a/pydoll/elements/web_element.py +++ b/pydoll/elements/web_element.py @@ -356,6 +356,7 @@ async def press_keyboard_key( async def _click_option_tag(self): """Specialized method for clicking