From 18dad5ea1c2a0be9df6880747652ded3d520b9fb Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 14:46:42 +0800
Subject: [PATCH 01/61] feat(test): add exact coverage tests for fingerprint
functionality
---
tests/test_exact_coverage.py | 858 +++++++++++++++++++++++++++++++++++
1 file changed, 858 insertions(+)
create mode 100644 tests/test_exact_coverage.py
diff --git a/tests/test_exact_coverage.py b/tests/test_exact_coverage.py
new file mode 100644
index 00000000..c5de4c39
--- /dev/null
+++ b/tests/test_exact_coverage.py
@@ -0,0 +1,858 @@
+"""
+Exact tests to cover uncovered lines reported by Codecov:
+- pydoll/fingerprint/generator.py#L335 (fallback browser type)
+- pydoll/fingerprint/manager.py#L55 (return current_fingerprint)
+- pydoll/fingerprint/manager.py#L262 (return False when file doesn't exist)
+- pydoll/browser/managers/browser_options_manager.py#L71 (early return when no fingerprint manager)
+
+This file merges all tests from test_additional_coverage.py and test_exact_coverage.py.
+"""
+
+import tempfile
+from pathlib import Path
+from unittest.mock import Mock, patch, AsyncMock
+
+from pydoll.fingerprint.generator import FingerprintGenerator
+from pydoll.fingerprint.manager import FingerprintManager
+from pydoll.fingerprint.models import FingerprintConfig
+from pydoll.browser.managers.browser_options_manager import ChromiumOptionsManager
+from pydoll.browser.options import ChromiumOptions
+
+
+class TestLine55EarlyReturn:
+ """Specific tests for the early return at line 55 in generate_new_fingerprint"""
+
+ def test_line_55_early_return_when_fingerprint_exists_and_no_force(self):
+ """
+ Test line 55: return self.current_fingerprint - early return in generate_new_fingerprint
+ This tests the case when current_fingerprint exists and force=False (default)
+ """
+ manager = FingerprintManager()
+
+ # First, generate a fingerprint to set current_fingerprint
+ first_fingerprint = manager.generate_new_fingerprint()
+ assert manager.current_fingerprint is first_fingerprint
+
+ # Now call generate_new_fingerprint again WITHOUT force=True
+ # This should trigger the early return at line 55
+ returned_fingerprint = manager.generate_new_fingerprint()
+
+ # Verify that line 55 was executed - the same fingerprint was returned
+ assert returned_fingerprint is first_fingerprint
+ assert returned_fingerprint is manager.current_fingerprint
+
+ # Verify no new fingerprint was generated (same object)
+ assert id(returned_fingerprint) == id(first_fingerprint)
+
+ def test_line_55_early_return_with_different_browser_types(self):
+ """
+ Test line 55 early return with different browser types
+ """
+ manager = FingerprintManager()
+
+ # Generate initial fingerprint with chrome
+ chrome_fingerprint = manager.generate_new_fingerprint(browser_type='chrome')
+ assert manager.current_fingerprint is chrome_fingerprint
+
+ # Call generate_new_fingerprint with edge but force=False (default)
+ # This should still trigger line 55 early return
+ returned_fingerprint = manager.generate_new_fingerprint(browser_type='edge')
+
+ # Line 55 should have been executed - same fingerprint returned
+ assert returned_fingerprint is chrome_fingerprint
+ assert manager.current_fingerprint is chrome_fingerprint
+
+ def test_line_55_early_return_multiple_calls(self):
+ """
+ Test line 55 early return with multiple sequential calls
+ """
+ manager = FingerprintManager()
+
+ # Generate initial fingerprint
+ original_fingerprint = manager.generate_new_fingerprint()
+
+ # Multiple calls without force should all trigger line 55
+ for i in range(5):
+ returned_fingerprint = manager.generate_new_fingerprint()
+
+ # Each call should execute line 55 and return the same fingerprint
+ assert returned_fingerprint is original_fingerprint
+ assert manager.current_fingerprint is original_fingerprint
+
+ def test_line_55_early_return_vs_force_generation(self):
+ """
+ Test the difference between line 55 early return and force generation
+ """
+ manager = FingerprintManager()
+
+ # Generate initial fingerprint
+ first_fingerprint = manager.generate_new_fingerprint()
+
+ # Call without force - should hit line 55
+ same_fingerprint = manager.generate_new_fingerprint(force=False)
+ assert same_fingerprint is first_fingerprint # Line 55 executed
+
+ # Call with force=True - should NOT hit line 55, creates new fingerprint
+ new_fingerprint = manager.generate_new_fingerprint(force=True)
+ assert new_fingerprint is not first_fingerprint # Line 55 NOT executed
+ assert manager.current_fingerprint is new_fingerprint
+
+ def test_line_55_early_return_with_explicit_force_false(self):
+ """
+ Test line 55 with explicitly setting force=False
+ """
+ manager = FingerprintManager()
+
+ # Generate initial fingerprint
+ initial_fingerprint = manager.generate_new_fingerprint()
+
+ # Explicitly set force=False to trigger line 55
+ returned_fingerprint = manager.generate_new_fingerprint(force=False)
+
+ # Verify line 55 was executed
+ assert returned_fingerprint is initial_fingerprint
+ assert manager.current_fingerprint is initial_fingerprint
+
+
+class TestLine55And262Coverage:
+ """Precise tests specifically targeting line 55 and line 262"""
+
+ def test_line_55_get_current_fingerprint_return_statement(self):
+ """
+ Specifically test manager.py line 55: return self.current_fingerprint
+ """
+ manager = FingerprintManager()
+
+ # Case 1: current_fingerprint is None
+ assert manager.current_fingerprint is None
+ result = manager.get_current_fingerprint() # Execute line 55
+ assert result is None
+
+ # Case 2: current_fingerprint exists
+ fingerprint = manager.generate_new_fingerprint()
+ assert manager.current_fingerprint is not None
+ result = manager.get_current_fingerprint() # Execute line 55 again
+ assert result is fingerprint
+ assert result is manager.current_fingerprint
+
+ def test_line_262_get_fingerprint_summary_return_statement(self):
+ """
+ Specifically test manager.py line 262: return { ... } - return statement of get_fingerprint_summary method
+ """
+ manager = FingerprintManager()
+
+ # Generate a fingerprint for testing summary
+ fingerprint = manager.generate_new_fingerprint()
+
+ # Call get_fingerprint_summary - this executes line 262's return { statement
+ summary = manager.get_fingerprint_summary()
+
+ # Verify the returned dictionary structure and content
+ assert isinstance(summary, dict)
+ expected_keys = [
+ 'Browser', 'User Agent', 'Platform', 'Language', 'Screen',
+ 'Viewport', 'WebGL Vendor', 'WebGL Renderer', 'Hardware Concurrency',
+ 'Device Memory', 'Timezone', 'Canvas Fingerprint'
+ ]
+ for key in expected_keys:
+ assert key in summary
+
+ # Verify specific content
+ assert fingerprint.browser_type.title() in summary['Browser']
+ assert summary['User Agent'] == fingerprint.user_agent
+ assert summary['Platform'] == fingerprint.platform
+
+ def test_line_262_with_explicit_fingerprint_parameter(self):
+ """
+ Test line 262 using explicit fingerprint parameter
+ """
+ manager = FingerprintManager()
+
+ # Generate two fingerprints
+ fp1 = manager.generate_new_fingerprint()
+ manager.clear_current_fingerprint()
+ fp2 = manager.generate_new_fingerprint(force=True)
+
+ # Call get_fingerprint_summary with explicit fingerprint parameter - executes line 262
+ summary1 = manager.get_fingerprint_summary(fp1)
+ summary2 = manager.get_fingerprint_summary(fp2)
+
+ # Verify summaries are different
+ assert summary1['User Agent'] != summary2['User Agent']
+ assert isinstance(summary1, dict)
+ assert isinstance(summary2, dict)
+
+ def test_both_lines_in_sequence(self):
+ """
+ Test both lines sequentially in one test
+ """
+ manager = FingerprintManager()
+
+ # Test line 55 - when None
+ result = manager.get_current_fingerprint() # Line 55
+ assert result is None
+
+ # Generate fingerprint
+ fingerprint = manager.generate_new_fingerprint()
+
+ # Test line 55 - when exists
+ current = manager.get_current_fingerprint() # Line 55
+ assert current is fingerprint
+
+ # Test line 262 - get_fingerprint_summary
+ summary = manager.get_fingerprint_summary() # Line 262
+ assert isinstance(summary, dict)
+ assert 'Browser' in summary
+ assert 'User Agent' in summary
+
+ def test_multiple_calls_ensure_line_coverage(self):
+ """
+ Multiple calls to ensure line coverage
+ """
+ manager = FingerprintManager()
+
+ # Multiple calls to line 55
+ for i in range(10):
+ result = manager.get_current_fingerprint() # Line 55
+ assert result is None
+
+ # Generate fingerprint
+ fingerprint = manager.generate_new_fingerprint()
+
+ # Multiple calls to line 55 and line 262
+ for i in range(10):
+ current = manager.get_current_fingerprint() # Line 55
+ assert current is fingerprint
+
+ summary = manager.get_fingerprint_summary() # Line 262
+ assert isinstance(summary, dict)
+
+ def test_line_262_edge_cases(self):
+ """
+ Test edge cases for line 262
+ """
+ manager = FingerprintManager()
+
+ # Use different browser types to generate fingerprints
+ browser_types = ['chrome', 'edge']
+
+ for browser_type in browser_types:
+ fingerprint = manager.generate_new_fingerprint(browser_type=browser_type, force=True)
+
+ # Test line 262 - return dictionary statement
+ summary = manager.get_fingerprint_summary() # Line 262
+
+ assert isinstance(summary, dict)
+ assert browser_type.title() in summary['Browser']
+
+ # Clear current fingerprint
+ manager.clear_current_fingerprint()
+
+ # Test line 262 again with explicit parameter
+ summary_explicit = manager.get_fingerprint_summary(fingerprint) # Line 262
+ assert summary == summary_explicit
+
+
+class TestDirectLineCoverage:
+ """Direct and simple tests for specific uncovered lines - merged from verification script"""
+
+ def test_line_55_when_none(self):
+ """
+ Direct test for line 55: return self.current_fingerprint (when None)
+ """
+ manager = FingerprintManager()
+
+ # Ensure current_fingerprint is None
+ assert manager.current_fingerprint is None
+
+ # Call get_current_fingerprint - EXECUTES LINE 55: return self.current_fingerprint
+ result = manager.get_current_fingerprint()
+
+ # Verify line 55 returned None correctly
+ assert result is None
+
+ def test_line_55_when_exists(self):
+ """
+ Direct test for line 55: return self.current_fingerprint (when exists)
+ """
+ manager = FingerprintManager()
+
+ # Generate a fingerprint to set current_fingerprint
+ fingerprint = manager.generate_new_fingerprint()
+ assert manager.current_fingerprint is not None
+ assert manager.current_fingerprint is fingerprint
+
+ # Call get_current_fingerprint - EXECUTES LINE 55: return self.current_fingerprint
+ result = manager.get_current_fingerprint()
+
+ # Verify line 55 was executed correctly
+ assert result is fingerprint
+ assert result is manager.current_fingerprint
+
+ def test_line_262_return_false(self):
+ """
+ Direct test for line 262: return False (when file doesn't exist)
+ """
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager = FingerprintManager()
+ manager.storage_dir = Path(temp_dir)
+
+ # Ensure directory is empty
+ assert len(list(manager.storage_dir.glob("*.json"))) == 0
+
+ # Try to delete non-existent file - EXECUTES return False LINE
+ result = manager.delete_fingerprint("nonexistent_file")
+
+ # Verify False was returned (line 262 executed)
+ assert result is False
+
+ def test_line_262_multiple_names(self):
+ """
+ Test line 262 with multiple different non-existent file names
+ """
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager = FingerprintManager()
+ manager.storage_dir = Path(temp_dir)
+
+ # Test various non-existent file names
+ test_names = ["fake", "test", "nonexistent", "", "long_name_that_doesnt_exist", "another_fake"]
+
+ for name in test_names:
+ # Each call should execute the return False line
+ result = manager.delete_fingerprint(name)
+ assert result is False
+
+ def test_comprehensive_workflow(self):
+ """
+ Complete workflow test that covers both lines 55 and 262
+ """
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager = FingerprintManager()
+ manager.storage_dir = Path(temp_dir)
+
+ # Test line 55 (when None)
+ assert manager.get_current_fingerprint() is None # Executes line 55
+
+ # Generate fingerprint
+ fp = manager.generate_new_fingerprint()
+
+ # Test line 55 (when not None)
+ current = manager.get_current_fingerprint() # Executes line 55
+ assert current is fp
+
+ # Test line 262
+ result = manager.delete_fingerprint("fake_file") # Executes return False line
+ assert result is False
+
+ # Test state changes and line 55 again
+ manager.clear_current_fingerprint()
+ assert manager.get_current_fingerprint() is None # Executes line 55
+
+ def test_multiple_calls_for_coverage_assurance(self):
+ """
+ Multiple calls to ensure the lines are definitely hit during coverage measurement
+ """
+ manager = FingerprintManager()
+
+ # Multiple calls to get_current_fingerprint (line 55) when None
+ for i in range(10):
+ result = manager.get_current_fingerprint()
+ assert result is None
+
+ # Generate fingerprint
+ fp = manager.generate_new_fingerprint()
+
+ # Multiple calls when fingerprint exists (line 55)
+ for i in range(10):
+ result = manager.get_current_fingerprint()
+ assert result is fp
+
+ # Multiple calls to delete_fingerprint (return False line 262)
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager.storage_dir = Path(temp_dir)
+ for i in range(10):
+ result = manager.delete_fingerprint(f"fake_{i}")
+ assert result is False
+
+ def test_state_transitions_line_55(self):
+ """
+ Test manager state transitions to thoroughly cover line 55
+ """
+ manager = FingerprintManager()
+
+ # Initial state - line 55 returns None
+ assert manager.get_current_fingerprint() is None
+ assert manager.get_current_fingerprint() is None # Call again
+
+ # After generating - line 55 returns fingerprint
+ fp1 = manager.generate_new_fingerprint()
+ assert manager.get_current_fingerprint() is fp1
+ assert manager.get_current_fingerprint() is fp1 # Call again
+
+ # After forcing new - line 55 returns new fingerprint
+ fp2 = manager.generate_new_fingerprint(force=True)
+ assert manager.get_current_fingerprint() is fp2
+ assert manager.get_current_fingerprint() is fp2 # Call again
+
+ # After clearing - line 55 returns None again
+ manager.clear_current_fingerprint()
+ assert manager.get_current_fingerprint() is None
+ assert manager.get_current_fingerprint() is None # Call again
+
+ def test_edge_cases_line_262(self):
+ """
+ Edge cases for line 262 to ensure complete coverage
+ """
+ # Test with different temporary directories
+ for i in range(5):
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager = FingerprintManager()
+ manager.storage_dir = Path(temp_dir)
+
+ # Test the return False line with different names
+ assert manager.delete_fingerprint(f"test_{i}") is False
+ assert manager.delete_fingerprint("") is False # Empty string
+ assert manager.delete_fingerprint("a" * 100) is False # Long name
+
+
+class TestExactLineCoverage:
+ """Additional focused tests for specific uncovered lines"""
+
+ def test_line_55_return_current_fingerprint_when_exists(self):
+ """
+ Test manager.py#L55 - get_current_fingerprint returns current fingerprint
+ Directly tests the return statement at line 55
+ """
+ manager = FingerprintManager()
+
+ # Ensure it starts with None
+ assert manager.current_fingerprint is None
+
+ # Generate a fingerprint to set current_fingerprint
+ fingerprint = manager.generate_new_fingerprint()
+ assert manager.current_fingerprint is not None
+ assert manager.current_fingerprint is fingerprint
+
+ # Call get_current_fingerprint - THIS EXECUTES LINE 55: return self.current_fingerprint
+ result = manager.get_current_fingerprint()
+
+ # Verify line 55 was executed correctly
+ assert result is fingerprint
+ assert result is manager.current_fingerprint
+
+ def test_line_55_return_current_fingerprint_when_none(self):
+ """
+ Additional test for line 55 when current_fingerprint is None
+ """
+ manager = FingerprintManager()
+
+ # Ensure current_fingerprint is None
+ assert manager.current_fingerprint is None
+
+ # Call get_current_fingerprint - THIS ALSO EXECUTES LINE 55: return self.current_fingerprint
+ result = manager.get_current_fingerprint()
+
+ # Verify line 55 returned None correctly
+ assert result is None
+
+ def test_line_262_delete_fingerprint_return_false(self):
+ """
+ Test manager.py#L262 - delete_fingerprint returns False when file doesn't exist
+ Directly tests the return False statement
+ """
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager = FingerprintManager()
+ manager.storage_dir = Path(temp_dir)
+
+ # Ensure directory is empty
+ assert len(list(manager.storage_dir.glob("*.json"))) == 0
+
+ # Try to delete non-existent file - THIS EXECUTES THE return False LINE
+ result = manager.delete_fingerprint("nonexistent_file")
+
+ # Verify False was returned (line 262 executed)
+ assert result is False
+
+ def test_line_262_with_various_nonexistent_names(self):
+ """
+ Additional tests for line 262 with different non-existent names
+ """
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager = FingerprintManager()
+ manager.storage_dir = Path(temp_dir)
+
+ # Test various non-existent file names
+ test_names = ["fake", "test", "nonexistent", "", "long_name_that_doesnt_exist"]
+
+ for name in test_names:
+ # Each call should execute the return False line
+ result = manager.delete_fingerprint(name)
+ assert result is False
+
+ def test_complete_workflow_covering_both_lines(self):
+ """
+ Complete test that covers both lines 55 and 262 in one workflow
+ """
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager = FingerprintManager()
+ manager.storage_dir = Path(temp_dir)
+
+ # Test line 55 (when None)
+ assert manager.get_current_fingerprint() is None # Executes line 55
+
+ # Generate fingerprint
+ fp = manager.generate_new_fingerprint()
+
+ # Test line 55 (when not None)
+ current = manager.get_current_fingerprint() # Executes line 55
+ assert current is fp
+
+ # Test line 262
+ result = manager.delete_fingerprint("fake") # Executes return False line
+ assert result is False
+
+ def test_multiple_calls_to_ensure_coverage(self):
+ """
+ Multiple calls to the methods to ensure the lines are definitely hit
+ """
+ manager = FingerprintManager()
+
+ # Multiple calls to get_current_fingerprint (line 55)
+ for _ in range(5):
+ result = manager.get_current_fingerprint()
+ assert result is None
+
+ # Generate fingerprint
+ fp = manager.generate_new_fingerprint()
+
+ # Multiple calls when fingerprint exists (line 55)
+ for _ in range(5):
+ result = manager.get_current_fingerprint()
+ assert result is fp
+
+ # Multiple calls to delete_fingerprint (return False line)
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager.storage_dir = Path(temp_dir)
+ for i in range(5):
+ result = manager.delete_fingerprint(f"fake_{i}")
+ assert result is False
+
+ def test_manager_state_changes_line_55(self):
+ """
+ Test state changes to ensure line 55 is covered in all scenarios
+ """
+ manager = FingerprintManager()
+
+ # Initial state - line 55 returns None
+ assert manager.get_current_fingerprint() is None
+
+ # After generating - line 55 returns fingerprint
+ fp1 = manager.generate_new_fingerprint()
+ assert manager.get_current_fingerprint() is fp1
+
+ # After forcing new - line 55 returns new fingerprint
+ fp2 = manager.generate_new_fingerprint(force=True)
+ assert manager.get_current_fingerprint() is fp2
+
+ # After clearing - line 55 returns None again
+ manager.clear_current_fingerprint()
+ assert manager.get_current_fingerprint() is None
+
+ def test_edge_case_scenarios(self):
+ """Test edge cases to ensure complete coverage"""
+
+ # Test with different temporary directories
+ for i in range(3):
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager = FingerprintManager()
+ manager.storage_dir = Path(temp_dir)
+
+ # Test the return False line
+ assert manager.delete_fingerprint(f"test_{i}") is False
+
+ # Test get_current_fingerprint in different scenarios
+ manager = FingerprintManager()
+
+ # Before any fingerprint
+ assert manager.get_current_fingerprint() is None
+
+ # With fingerprint
+ fp = manager.generate_new_fingerprint()
+ assert manager.get_current_fingerprint() is fp
+
+ # Multiple calls
+ assert manager.get_current_fingerprint() is fp
+ assert manager.get_current_fingerprint() is fp
+
+
+class TestExactCoverage:
+ """Legacy tests merged for compatibility"""
+
+ def test_generator_line_335_fallback_browser_type(self):
+ """
+ Test generator.py#L335 - _generate_user_agent fallback case
+ when browser_type is neither 'chrome' nor 'edge'
+ """
+ # Create a configuration with browser_type set to non-chrome/edge value
+ config = FingerprintConfig()
+ config.browser_type = 'firefox' # This triggers fallback to Chrome
+ config.is_mobile = False
+
+ generator = FingerprintGenerator(config)
+
+ # Mock OS info and browser version
+ os_info = {'name': 'Windows', 'version': '10.0'}
+ browser_version = '120.0.6099.129'
+
+ # Call _generate_user_agent, this should hit line 335 fallback
+ user_agent = generator._generate_user_agent(os_info, browser_version)
+
+ # Verify Chrome user agent was generated (fallback behavior)
+ assert 'Chrome' in user_agent
+ assert 'Windows NT 10.0' in user_agent
+ assert browser_version in user_agent
+
+ def test_generator_unique_properties_method(self):
+ """Test _generate_unique_properties static method call"""
+ # Call static method directly to ensure execution
+ result = FingerprintGenerator._generate_unique_properties()
+
+ # Verify return value structure
+ assert isinstance(result, dict)
+ assert "unique_id" in result
+ assert isinstance(result["unique_id"], str)
+ assert len(result["unique_id"]) > 0
+
+ # Call multiple times to ensure different values are generated
+ result2 = FingerprintGenerator._generate_unique_properties()
+ assert result["unique_id"] != result2["unique_id"]
+
+ def test_browser_options_manager_line_71_no_fingerprint_manager(self):
+ """Test browser_options_manager.py#L71 - early return when no fingerprint manager"""
+ # Create manager without fingerprint spoofing enabled
+ manager = ChromiumOptionsManager(enable_fingerprint_spoofing=False)
+ manager.options = ChromiumOptions()
+
+ # Manually set fingerprint_manager to None to ensure we hit line 71
+ manager.fingerprint_manager = None
+
+ # Call _apply_fingerprint_spoofing - should return early at line 71
+ result = manager._apply_fingerprint_spoofing()
+ assert result is None
+
+ def test_comprehensive_integration_all_lines(self):
+ """
+ Final comprehensive test that ensures all target lines are covered
+ """
+ # Test generator fallback (line 335)
+ config = FingerprintConfig()
+ config.browser_type = 'unsupported_browser'
+ config.is_mobile = False
+ generator = FingerprintGenerator(config)
+
+ os_info = {'name': 'Windows', 'version': '10.0'}
+ user_agent = generator._generate_user_agent(os_info, '120.0.0.0')
+ assert 'Chrome' in user_agent # Should fallback to Chrome
+
+ # Test unique properties
+ unique_props = FingerprintGenerator._generate_unique_properties()
+ assert "unique_id" in unique_props
+
+ # Test manager lines 55 and 262
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager = FingerprintManager()
+ manager.storage_dir = Path(temp_dir)
+
+ # Line 55 tests
+ assert manager.get_current_fingerprint() is None # Line 55
+ fp = manager.generate_new_fingerprint()
+ assert manager.get_current_fingerprint() is fp # Line 55
+
+ # Test the early return line 55 in generate_new_fingerprint
+ same_fp = manager.generate_new_fingerprint() # Should execute line 55 early return
+ assert same_fp is fp
+
+ # Test get_fingerprint_summary (this might be line 262)
+ summary = manager.get_fingerprint_summary() # Possible line 262
+ assert isinstance(summary, dict)
+
+ # Line 262 test for delete_fingerprint
+ assert manager.delete_fingerprint("nonexistent") is False # Possible line 262
+
+ # Test browser options manager (line 71)
+ options_manager = ChromiumOptionsManager(enable_fingerprint_spoofing=False)
+ options_manager.fingerprint_manager = None
+ result = options_manager._apply_fingerprint_spoofing()
+ assert result is None
+
+ def test_browser_type_variations_for_fallback(self):
+ """
+ Test various browser_type values to ensure fallback is triggered
+ """
+ test_cases = [
+ 'firefox',
+ 'safari',
+ 'opera',
+ 'unknown',
+ 'invalid',
+ None, # If None, should also trigger fallback
+ ]
+
+ for browser_type in test_cases:
+ config = FingerprintConfig()
+ config.browser_type = browser_type
+ config.is_mobile = False
+
+ generator = FingerprintGenerator(config)
+
+ os_info = {'name': 'Windows', 'version': '10.0'}
+ browser_version = '120.0.0.0'
+
+ # This should all trigger line 335 fallback
+ user_agent = generator._generate_user_agent(os_info, browser_version)
+
+ # Verify all fallback to Chrome
+ assert 'Chrome' in user_agent
+ assert 'Windows NT 10.0' in user_agent
+
+ def test_manager_state_transitions(self):
+ """
+ Test manager state transitions to cover line 55
+ """
+ manager = FingerprintManager()
+
+ # Initial state: None
+ assert manager.get_current_fingerprint() is None
+
+ # After generating fingerprint
+ fp1 = manager.generate_new_fingerprint()
+ assert manager.get_current_fingerprint() is fp1 # Hit line 55
+
+ # Force generate new fingerprint
+ fp2 = manager.generate_new_fingerprint(force=True)
+ assert manager.get_current_fingerprint() is fp2 # Hit line 55 again
+
+ # After clearing
+ manager.clear_current_fingerprint()
+ assert manager.get_current_fingerprint() is None
+
+ def test_delete_operations_coverage(self):
+ """
+ Test delete operations to cover line 262
+ """
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager = FingerprintManager()
+ manager.storage_dir = Path(temp_dir)
+
+ # Try to delete different non-existent files
+ test_names = [
+ "nonexistent1",
+ "nonexistent2",
+ "fake_fingerprint",
+ "", # Empty string
+ "very_long_name_that_definitely_does_not_exist",
+ ]
+
+ for name in test_names:
+ # Each should hit line 262 return False
+ result = manager.delete_fingerprint(name)
+ assert result is False
+
+ def test_edge_cases_for_complete_coverage(self):
+ """Additional edge case tests to ensure 100% coverage of all target lines"""
+
+ # Test multiple calls to unique properties
+ props1 = FingerprintGenerator._generate_unique_properties()
+ props2 = FingerprintGenerator._generate_unique_properties()
+ props3 = FingerprintGenerator._generate_unique_properties()
+
+ # Ensure they are all different
+ unique_ids = [props1["unique_id"], props2["unique_id"], props3["unique_id"]]
+ assert len(set(unique_ids)) == 3 # All should be unique
+
+ # Test manager state transitions
+ manager = FingerprintManager()
+
+ # Multiple calls when None
+ assert manager.get_current_fingerprint() is None
+ assert manager.get_current_fingerprint() is None
+
+ # Test multiple calls after generation
+ fp = manager.generate_new_fingerprint()
+ assert manager.get_current_fingerprint() is fp
+ assert manager.get_current_fingerprint() is fp
+
+ # Clear and test again
+ manager.clear_current_fingerprint()
+ assert manager.get_current_fingerprint() is None
+
+ # Test delete operations
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager.storage_dir = Path(temp_dir)
+
+ # Multiple attempts to delete non-existent files
+ assert manager.delete_fingerprint("file1") is False
+ assert manager.delete_fingerprint("file2") is False
+ assert manager.delete_fingerprint("") is False
+
+ def test_fingerprint_integration_complete_workflow(self):
+ """Complete fingerprint integration workflow test"""
+ # Test complete workflow that should cover all lines
+
+ # 1. Test unique properties generation
+ unique_props = FingerprintGenerator._generate_unique_properties()
+ assert "unique_id" in unique_props
+
+ # 2. Test fingerprint manager workflow (line 55 and 262)
+ with tempfile.TemporaryDirectory() as temp_dir:
+ manager = FingerprintManager()
+ manager.storage_dir = Path(temp_dir)
+
+ # Initially no fingerprint
+ assert manager.get_current_fingerprint() is None
+
+ # Generate one
+ fingerprint = manager.generate_new_fingerprint()
+
+ # Test line 55 - return current fingerprint
+ current = manager.get_current_fingerprint()
+ assert current is fingerprint
+
+ # Test line 262 - delete non-existent file
+ assert manager.delete_fingerprint("fake") is False
+
+ # 3. Test browser options manager (line 71)
+ options_manager = ChromiumOptionsManager(enable_fingerprint_spoofing=False)
+ options_manager.fingerprint_manager = None
+ result = options_manager._apply_fingerprint_spoofing()
+ assert result is None
+
+ # 4. Test generator fallback (line 335)
+ config = FingerprintConfig()
+ config.browser_type = 'unsupported_browser'
+ config.is_mobile = False
+ generator = FingerprintGenerator(config)
+
+ os_info = {'name': 'Windows', 'version': '10.0'}
+ user_agent = generator._generate_user_agent(os_info, '120.0.0.0')
+ assert 'Chrome' in user_agent # Should fallback to Chrome
+
+ def test_various_browser_options_scenarios(self):
+ """Test various browser options scenarios"""
+ # Test fingerprint spoofing enabled but manually set to None
+ manager_enabled = ChromiumOptionsManager(enable_fingerprint_spoofing=True)
+ manager_enabled.options = ChromiumOptions()
+ manager_enabled.fingerprint_manager = None # Manually set to None
+
+ # This should hit line 71 early return
+ result = manager_enabled._apply_fingerprint_spoofing()
+ assert result is None
+
+ # Test fingerprint spoofing disabled
+ manager_disabled = ChromiumOptionsManager(enable_fingerprint_spoofing=False)
+ manager_disabled.options = ChromiumOptions()
+ assert manager_disabled.fingerprint_manager is None
+
+ # This should also hit line 71
+ result = manager_disabled._apply_fingerprint_spoofing()
+ assert result is None
\ No newline at end of file
From ae30976e5f54c8e1388d4b347984cdc4ad783882 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 14:51:44 +0800
Subject: [PATCH 02/61] feat(test): add comprehensive fingerprint core
functionality tests
---
tests/test_fingerprint.py | 394 ++++++++++++++++++++++++++++++++++++++
1 file changed, 394 insertions(+)
create mode 100644 tests/test_fingerprint.py
diff --git a/tests/test_fingerprint.py b/tests/test_fingerprint.py
new file mode 100644
index 00000000..f2c40b28
--- /dev/null
+++ b/tests/test_fingerprint.py
@@ -0,0 +1,394 @@
+"""
+Tests for the fingerprint spoofing functionality.
+
+This module contains unit tests for the fingerprint spoofing components
+of the pydoll library.
+"""
+
+import os
+import json
+import pytest
+from unittest.mock import Mock, patch
+
+from pydoll.fingerprint import (
+ FingerprintGenerator,
+ FingerprintInjector,
+ FingerprintManager,
+ Fingerprint,
+ FingerprintConfig,
+)
+
+
+class TestFingerprintConfig:
+ """Test the FingerprintConfig class."""
+
+ def test_default_config(self):
+ """Test default configuration values."""
+ config = FingerprintConfig()
+ assert config.browser_type == "chrome"
+ assert config.is_mobile == False
+ assert config.min_screen_width == 1024
+ assert config.enable_webgl_spoofing == True
+
+ def test_custom_config(self):
+ """Test custom configuration."""
+ config = FingerprintConfig(
+ browser_type="edge",
+ preferred_os="windows",
+ enable_webgl_spoofing=False
+ )
+ assert config.browser_type == "edge"
+ assert config.preferred_os == "windows"
+ assert config.enable_webgl_spoofing == False
+
+ def test_config_serialization(self):
+ """Test configuration to/from dict conversion."""
+ config = FingerprintConfig(browser_type="edge", is_mobile=True)
+ config_dict = config.to_dict()
+ restored_config = FingerprintConfig.from_dict(config_dict)
+
+ assert restored_config.browser_type == config.browser_type
+ assert restored_config.is_mobile == config.is_mobile
+
+
+class TestFingerprintGenerator:
+ """Test the FingerprintGenerator class."""
+
+ def test_default_generation(self):
+ """Test generating a fingerprint with default settings."""
+ generator = FingerprintGenerator()
+ fingerprint = generator.generate()
+
+ assert isinstance(fingerprint, Fingerprint)
+ assert fingerprint.user_agent
+ assert fingerprint.platform
+ assert fingerprint.screen_width > 0
+ assert fingerprint.screen_height > 0
+ assert fingerprint.hardware_concurrency > 0
+ assert len(fingerprint.webgl_extensions) > 0
+
+ def test_chrome_generation(self):
+ """Test generating a Chrome fingerprint."""
+ config = FingerprintConfig(browser_type="chrome")
+ generator = FingerprintGenerator(config)
+ fingerprint = generator.generate()
+
+ assert fingerprint.browser_type == "chrome"
+ assert "Chrome" in fingerprint.user_agent
+
+ def test_edge_generation(self):
+ """Test generating an Edge fingerprint."""
+ config = FingerprintConfig(browser_type="edge")
+ generator = FingerprintGenerator(config)
+ fingerprint = generator.generate()
+
+ assert fingerprint.browser_type == "edge"
+ assert "Edg" in fingerprint.user_agent
+
+ def test_consistent_generation(self):
+ """Test that multiple generations produce different fingerprints."""
+ generator = FingerprintGenerator()
+ fp1 = generator.generate()
+ fp2 = generator.generate()
+
+ # Should be different (random generation)
+ # At least user agent should be different due to version randomization
+ assert fp1.canvas_fingerprint != fp2.canvas_fingerprint
+
+ def test_os_specific_generation(self):
+ """Test OS-specific fingerprint generation."""
+ # Test with preferred OS setting
+ config_win = FingerprintConfig(preferred_os="windows")
+ generator_win = FingerprintGenerator(config_win)
+ fp_win = generator_win.generate()
+
+ # Just verify that a fingerprint is generated
+ assert fp_win.platform is not None
+ assert fp_win.user_agent is not None
+
+ # Test with another preferred OS setting
+ config_mac = FingerprintConfig(preferred_os="macos")
+ generator_mac = FingerprintGenerator(config_mac)
+ fp_mac = generator_mac.generate()
+
+ # Just verify that a fingerprint is generated
+ assert fp_mac.platform is not None
+ assert fp_mac.user_agent is not None
+
+ # Note: The actual OS preference may not be enforced in the current implementation
+ # This test just verifies that fingerprints can be generated with these settings
+
+ def test_mobile_generation(self):
+ """Test mobile fingerprint generation."""
+ config = FingerprintConfig(is_mobile=True)
+ generator = FingerprintGenerator(config)
+ fingerprint = generator.generate()
+
+ # Just verify that a fingerprint is generated
+ assert fingerprint.screen_width > 0
+ assert fingerprint.screen_height > 0
+
+ # Note: The mobile setting may not be enforced in the current implementation
+ # This test just verifies that fingerprints can be generated with this setting
+
+
+class TestFingerprintInjector:
+ """Test the FingerprintInjector class."""
+
+ def test_script_generation(self):
+ """Test JavaScript script generation."""
+ # Create a test fingerprint
+ fingerprint = Fingerprint(
+ user_agent="Test User Agent",
+ platform="Win32",
+ language="en-US",
+ languages=["en-US", "en"],
+ hardware_concurrency=4,
+ screen_width=1920,
+ screen_height=1080,
+ screen_color_depth=24,
+ screen_pixel_depth=24,
+ available_width=1920,
+ available_height=1040,
+ viewport_width=1200,
+ viewport_height=800,
+ inner_width=1200,
+ inner_height=680,
+ webgl_vendor="Google Inc.",
+ webgl_renderer="Test Renderer",
+ webgl_version="WebGL 1.0",
+ webgl_shading_language_version="WebGL GLSL ES 1.0",
+ webgl_extensions=["EXT_test"],
+ canvas_fingerprint="test_canvas_123",
+ audio_context_sample_rate=44100.0,
+ audio_context_state="suspended",
+ audio_context_max_channel_count=2,
+ timezone="America/New_York",
+ timezone_offset=-300,
+ browser_type="chrome",
+ browser_version="120.0.0.0",
+ plugins=[],
+ )
+
+ injector = FingerprintInjector(fingerprint)
+ script = injector.generate_script()
+
+ assert isinstance(script, str)
+ assert len(script) > 0
+ assert "userAgent" in script
+ assert "Test User Agent" in script
+ assert "width" in script
+ assert "1920" in script
+ assert "webdriver" in script
+
+ def test_navigator_override(self):
+ """Test navigator properties override generation."""
+ fingerprint = Fingerprint(
+ user_agent="Test UA",
+ platform="Test Platform",
+ language="en-US",
+ languages=["en-US"],
+ hardware_concurrency=8,
+ screen_width=1920, screen_height=1080,
+ screen_color_depth=24, screen_pixel_depth=24,
+ available_width=1920, available_height=1040,
+ viewport_width=1200, viewport_height=800,
+ inner_width=1200, inner_height=680,
+ webgl_vendor="Test", webgl_renderer="Test",
+ webgl_version="Test", webgl_shading_language_version="Test",
+ webgl_extensions=[], canvas_fingerprint="test",
+ audio_context_sample_rate=44100.0, audio_context_state="suspended",
+ audio_context_max_channel_count=2, timezone="UTC", timezone_offset=0,
+ browser_type="chrome", browser_version="120.0.0.0", plugins=[],
+ )
+
+ injector = FingerprintInjector(fingerprint)
+ nav_script = injector._generate_navigator_override()
+
+ assert "Test UA" in nav_script
+ assert "Test Platform" in nav_script
+ assert "hardwareConcurrency" in nav_script
+ assert "8" in nav_script
+
+ def test_webgl_override(self):
+ """Test WebGL override script generation."""
+ fingerprint = Fingerprint(
+ user_agent="Test UA",
+ platform="Test Platform",
+ language="en-US",
+ languages=["en-US"],
+ hardware_concurrency=8,
+ screen_width=1920, screen_height=1080,
+ screen_color_depth=24, screen_pixel_depth=24,
+ available_width=1920, available_height=1040,
+ viewport_width=1200, viewport_height=800,
+ inner_width=1200, inner_height=680,
+ webgl_vendor="Custom WebGL Vendor",
+ webgl_renderer="Custom WebGL Renderer",
+ webgl_version="WebGL 2.0",
+ webgl_shading_language_version="WebGL GLSL ES 3.0",
+ webgl_extensions=["EXT_test1", "EXT_test2"],
+ canvas_fingerprint="test",
+ audio_context_sample_rate=44100.0, audio_context_state="suspended",
+ audio_context_max_channel_count=2, timezone="UTC", timezone_offset=0,
+ browser_type="chrome", browser_version="120.0.0.0", plugins=[],
+ )
+
+ injector = FingerprintInjector(fingerprint)
+ webgl_script = injector._generate_webgl_override()
+
+ assert "Custom WebGL Vendor" in webgl_script
+ assert "Custom WebGL Renderer" in webgl_script
+ assert "WebGL 2.0" in webgl_script
+ assert "EXT_test1" in webgl_script
+ assert "EXT_test2" in webgl_script
+
+
+class TestFingerprintManager:
+ """Test the FingerprintManager class."""
+
+ def test_manager_initialization(self):
+ """Test manager initialization."""
+ manager = FingerprintManager()
+ assert manager.current_fingerprint is None
+ assert manager.injector is None
+
+ def test_fingerprint_generation(self):
+ """Test fingerprint generation through manager."""
+ manager = FingerprintManager()
+ fingerprint = manager.generate_new_fingerprint("chrome")
+
+ assert manager.current_fingerprint is not None
+ assert manager.injector is not None
+ assert fingerprint.browser_type == "chrome"
+
+ def test_javascript_generation(self):
+ """Test JavaScript generation through manager."""
+ manager = FingerprintManager()
+ manager.generate_new_fingerprint("chrome")
+
+ js_code = manager.get_fingerprint_js()
+ assert isinstance(js_code, str)
+ assert len(js_code) > 0
+
+ def test_argument_generation(self):
+ """Test command line argument generation."""
+ manager = FingerprintManager()
+ manager.generate_new_fingerprint("chrome")
+
+ args = manager.get_fingerprint_arguments("chrome")
+ assert isinstance(args, list)
+ assert len(args) > 0
+ assert any("--user-agent=" in arg for arg in args)
+ assert any("--disable-blink-features=AutomationControlled" in arg for arg in args)
+
+ @patch('pathlib.Path.mkdir')
+ @patch('builtins.open')
+ @patch('json.dump')
+ def test_save_fingerprint(self, mock_json_dump, mock_open, mock_mkdir):
+ """Test saving fingerprint to disk."""
+ manager = FingerprintManager()
+ manager.generate_new_fingerprint("chrome")
+
+ # Mock file operations
+ mock_open.return_value.__enter__.return_value = Mock()
+
+ result = manager.save_fingerprint("test_fp")
+
+ assert isinstance(result, str)
+ assert "test_fp.json" in result
+ mock_json_dump.assert_called_once()
+
+ @patch('pathlib.Path.exists')
+ @patch('builtins.open')
+ @patch('json.load')
+ def test_load_fingerprint(self, mock_json_load, mock_open, mock_exists):
+ """Test loading fingerprint from disk."""
+ manager = FingerprintManager()
+
+ # Mock file operations
+ mock_exists.return_value = True
+ mock_open.return_value.__enter__.return_value = Mock()
+ mock_json_load.return_value = {
+ "user_agent": "Test UA",
+ "platform": "Test Platform",
+ "language": "en-US",
+ "languages": ["en-US"],
+ "hardware_concurrency": 8,
+ "screen_width": 1920,
+ "screen_height": 1080,
+ "screen_color_depth": 24,
+ "screen_pixel_depth": 24,
+ "available_width": 1920,
+ "available_height": 1040,
+ "viewport_width": 1200,
+ "viewport_height": 800,
+ "inner_width": 1200,
+ "inner_height": 680,
+ "webgl_vendor": "Test",
+ "webgl_renderer": "Test",
+ "webgl_version": "Test",
+ "webgl_shading_language_version": "Test",
+ "webgl_extensions": [],
+ "canvas_fingerprint": "test",
+ "audio_context_sample_rate": 44100.0,
+ "audio_context_state": "suspended",
+ "audio_context_max_channel_count": 2,
+ "timezone": "UTC",
+ "timezone_offset": 0,
+ "browser_type": "chrome",
+ "browser_version": "120.0.0.0",
+ "plugins": [],
+ }
+
+ fingerprint = manager.load_fingerprint("test_fp")
+
+ assert fingerprint is not None
+ assert fingerprint.user_agent == "Test UA"
+ assert fingerprint.platform == "Test Platform"
+
+ def test_error_without_fingerprint(self):
+ """Test error handling when no fingerprint is generated."""
+ manager = FingerprintManager()
+
+ with pytest.raises(ValueError, match="No fingerprint has been generated"):
+ manager.get_fingerprint_js()
+
+ with pytest.raises(ValueError, match="No fingerprint has been generated"):
+ manager.get_fingerprint_arguments()
+
+
+class TestFingerprint:
+ """Test the Fingerprint data class."""
+
+ def test_fingerprint_serialization(self):
+ """Test fingerprint to/from dict conversion."""
+ fingerprint = Fingerprint(
+ user_agent="Test",
+ platform="Test",
+ language="en",
+ languages=["en"],
+ hardware_concurrency=4,
+ screen_width=1920, screen_height=1080,
+ screen_color_depth=24, screen_pixel_depth=24,
+ available_width=1920, available_height=1040,
+ viewport_width=1200, viewport_height=800,
+ inner_width=1200, inner_height=680,
+ webgl_vendor="Test", webgl_renderer="Test",
+ webgl_version="Test", webgl_shading_language_version="Test",
+ webgl_extensions=[], canvas_fingerprint="test",
+ audio_context_sample_rate=44100.0, audio_context_state="suspended",
+ audio_context_max_channel_count=2, timezone="UTC", timezone_offset=0,
+ browser_type="chrome", browser_version="120.0.0.0", plugins=[],
+ )
+
+ fp_dict = fingerprint.to_dict()
+ restored_fp = Fingerprint.from_dict(fp_dict)
+
+ assert restored_fp.user_agent == fingerprint.user_agent
+ assert restored_fp.screen_width == fingerprint.screen_width
+ assert restored_fp.hardware_concurrency == fingerprint.hardware_concurrency
+
+
+if __name__ == "__main__":
+ pytest.main([__file__])
\ No newline at end of file
From 80d4efbb195bc8d3860579e36ada54448bad3de3 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 14:52:05 +0800
Subject: [PATCH 03/61] feat(test): add fingerprint coverage-specific tests for
100% code coverage
---
tests/test_fingerprint_coverage.py | 251 +++++++++++++++++++++++++++++
1 file changed, 251 insertions(+)
create mode 100644 tests/test_fingerprint_coverage.py
diff --git a/tests/test_fingerprint_coverage.py b/tests/test_fingerprint_coverage.py
new file mode 100644
index 00000000..66a68276
--- /dev/null
+++ b/tests/test_fingerprint_coverage.py
@@ -0,0 +1,251 @@
+"""
+Tests to achieve 100% code coverage for fingerprint functionality.
+Specifically targets uncovered lines in PR #129.
+"""
+
+import tempfile
+from pathlib import Path
+from unittest.mock import Mock, patch, AsyncMock
+import pytest
+
+from pydoll.browser.chromium.chrome import Chrome
+from pydoll.browser.managers.browser_options_manager import ChromiumOptionsManager
+from pydoll.browser.options import ChromiumOptions
+from pydoll.fingerprint import (
+ FingerprintConfig,
+ FingerprintManager,
+ FingerprintGenerator,
+)
+
+
+class TestCoverageSpecific:
+ """Tests to cover specific uncovered lines."""
+
+ def test_browser_options_apply_fingerprint_spoofing(self):
+ """Test _apply_fingerprint_spoofing method - normal case"""
+ config = FingerprintConfig(browser_type="chrome")
+ manager = ChromiumOptionsManager(
+ enable_fingerprint_spoofing=True,
+ fingerprint_config=config
+ )
+
+ # Set up options
+ manager.options = ChromiumOptions()
+ manager.options.binary_location = "/path/to/chrome"
+
+ # Ensure fingerprint manager exists and mock its methods
+ assert manager.fingerprint_manager is not None
+ manager.fingerprint_manager.generate_new_fingerprint = Mock()
+ manager.fingerprint_manager.get_fingerprint_arguments = Mock(return_value=[
+ '--user-agent=Test Agent'
+ ])
+
+ # Call the method that contains uncovered line
+ manager._apply_fingerprint_spoofing()
+
+ # Verify the method was called
+ manager.fingerprint_manager.generate_new_fingerprint.assert_called_with('chrome')
+ manager.fingerprint_manager.get_fingerprint_arguments.assert_called_with('chrome')
+
+ def test_browser_options_apply_fingerprint_spoofing_no_manager(self):
+ """Test _apply_fingerprint_spoofing method when manager is None - covers browser_options_manager.py#L71"""
+ manager = ChromiumOptionsManager(enable_fingerprint_spoofing=False)
+
+ # Set up options
+ manager.options = ChromiumOptions()
+
+ # Ensure fingerprint manager is None
+ manager.fingerprint_manager = None
+
+ # Call the method - this should hit the early return on line 71
+ result = manager._apply_fingerprint_spoofing()
+
+ # Should return None without error
+ assert result is None
+
+ def test_fingerprint_generator_unique_properties(self):
+ """Test _generate_unique_properties - covers generator.py#L335"""
+ # This covers the uncovered line in generator.py
+ unique_props = FingerprintGenerator._generate_unique_properties()
+
+ assert "unique_id" in unique_props
+ assert isinstance(unique_props["unique_id"], str)
+ assert len(unique_props["unique_id"]) > 0
+
+ def test_fingerprint_manager_get_current_fingerprint_none(self):
+ """Test get_current_fingerprint when None"""
+ manager = FingerprintManager()
+
+ # Initially should be None
+ current = manager.get_current_fingerprint()
+ assert current is None
+
+ def test_fingerprint_manager_get_current_fingerprint_exists(self):
+ """Test get_current_fingerprint when fingerprint exists - covers manager.py#L55"""
+ manager = FingerprintManager()
+
+ # Generate a fingerprint first
+ fingerprint = manager.generate_new_fingerprint()
+
+ # Now get the current fingerprint - this should hit line 55
+ current = manager.get_current_fingerprint()
+ assert current is fingerprint
+ assert current is not None
+
+ def test_fingerprint_manager_delete_nonexistent_file(self):
+ """Test delete_fingerprint with nonexistent file - covers manager.py#L262"""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ config = FingerprintConfig()
+ manager = FingerprintManager(config)
+ manager.storage_dir = Path(temp_dir)
+
+ # Try to delete non-existent file - this covers line 262
+ result = manager.delete_fingerprint("nonexistent")
+ assert result is False
+
+ @pytest.mark.asyncio
+ async def test_browser_start_with_fingerprint_injection(self):
+ """Test browser.start() with fingerprint injection - covers base.py#L137"""
+ with patch('pydoll.browser.chromium.chrome.Chrome._get_default_binary_location'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._setup_user_dir'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._verify_browser_running'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._configure_proxy'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._get_valid_tab_id', return_value="test_id"), \
+ patch('pydoll.browser.tab.Tab') as mock_tab_class, \
+ patch('pydoll.browser.chromium.chrome.Chrome._inject_fingerprint_script') as mock_inject:
+
+ # Create mock tab
+ mock_tab = Mock()
+ mock_tab._execute_command = AsyncMock()
+ mock_tab_class.return_value = mock_tab
+
+ # Create Chrome with fingerprint spoofing enabled
+ chrome = Chrome(enable_fingerprint_spoofing=True)
+
+ # Mock all necessary components
+ chrome._browser_process_manager = Mock()
+ chrome._browser_process_manager.start_process = Mock()
+ chrome._connection_handler = Mock()
+ chrome._connection_handler.connect = AsyncMock()
+ chrome._execute_command = AsyncMock()
+
+ # This should trigger the fingerprint injection on line 137
+ await chrome.start()
+
+ # Verify fingerprint injection was called (covers line 137)
+ mock_inject.assert_called_once()
+
+ @pytest.mark.asyncio
+ async def test_browser_new_tab_with_fingerprint_injection(self):
+ """Test browser.new_tab() with fingerprint injection - covers base.py#L226"""
+ with patch('pydoll.browser.chromium.chrome.Chrome._get_default_binary_location'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._setup_user_dir'), \
+ patch('pydoll.browser.tab.Tab') as mock_tab_class, \
+ patch('pydoll.browser.chromium.chrome.Chrome._inject_fingerprint_script') as mock_inject:
+
+ # Create mock tab
+ mock_tab = Mock()
+ mock_tab._execute_command = AsyncMock()
+ mock_tab_class.return_value = mock_tab
+
+ # Create Chrome with fingerprint spoofing enabled
+ chrome = Chrome(enable_fingerprint_spoofing=True)
+
+ # Mock the execute command to return a valid response
+ chrome._execute_command = AsyncMock(return_value={
+ 'result': {'targetId': 'test_target_id'}
+ })
+
+ # This should trigger the fingerprint injection on line 226
+ await chrome.new_tab(url="https://example.com")
+
+ # Verify fingerprint injection was called (covers line 226)
+ mock_inject.assert_called_once()
+
+ @pytest.mark.asyncio
+ async def test_browser_fingerprint_injection_method_coverage(self):
+ """Test the actual _inject_fingerprint_script method to ensure it's covered"""
+ with patch('pydoll.browser.chromium.chrome.Chrome._get_default_binary_location'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._setup_user_dir'):
+
+ chrome = Chrome(enable_fingerprint_spoofing=True)
+
+ # Create mock tab
+ mock_tab = Mock()
+ mock_tab._execute_command = AsyncMock()
+ mock_tab.execute_script = AsyncMock()
+
+ # Ensure fingerprint manager exists and mock its method
+ assert chrome.fingerprint_manager is not None
+ chrome.fingerprint_manager.get_fingerprint_js = Mock(return_value="test_script")
+
+ # Call the method directly
+ await chrome._inject_fingerprint_script(mock_tab)
+
+ # Verify both script injection methods were called
+ mock_tab._execute_command.assert_called()
+ mock_tab.execute_script.assert_called_with("test_script")
+
+ @pytest.mark.asyncio
+ async def test_fingerprint_injection_with_exception_handling(self):
+ """Test fingerprint injection with exception handling"""
+ with patch('pydoll.browser.chromium.chrome.Chrome._get_default_binary_location'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._setup_user_dir'):
+
+ chrome = Chrome(enable_fingerprint_spoofing=True)
+
+ # Create mock tab that raises exception
+ mock_tab = Mock()
+ mock_tab._execute_command = AsyncMock()
+ mock_tab.execute_script = AsyncMock(side_effect=Exception("Test exception"))
+
+ # Ensure fingerprint manager exists
+ assert chrome.fingerprint_manager is not None
+ chrome.fingerprint_manager.get_fingerprint_js = Mock(return_value="test_script")
+
+ # Should not raise exception
+ await chrome._inject_fingerprint_script(mock_tab)
+
+ # Verify that both attempts were made despite the exception
+ mock_tab._execute_command.assert_called()
+ mock_tab.execute_script.assert_called_with("test_script")
+
+ def test_fingerprint_manager_edge_case_coverage(self):
+ """Test additional edge cases for fingerprint manager"""
+ manager = FingerprintManager()
+
+ # Test getting current fingerprint when None
+ assert manager.get_current_fingerprint() is None
+
+ # Generate a fingerprint
+ fingerprint = manager.generate_new_fingerprint()
+ assert fingerprint is not None
+
+ # Now test getting current fingerprint when it exists
+ current = manager.get_current_fingerprint()
+ assert current is fingerprint
+
+ # Test clear functionality
+ manager.clear_current_fingerprint()
+ assert manager.current_fingerprint is None
+ assert manager.injector is None
+
+ def test_options_manager_edge_cases(self):
+ """Test edge cases in options manager"""
+ # Test with Chrome-style binary path
+ manager = ChromiumOptionsManager(enable_fingerprint_spoofing=True)
+ manager.options = ChromiumOptions()
+ manager.options.binary_location = "/usr/bin/google-chrome"
+
+ browser_type = manager._detect_browser_type()
+ assert browser_type == 'chrome'
+
+ # Test with Edge-style binary path
+ manager.options.binary_location = "/usr/bin/microsoft-edge"
+ browser_type = manager._detect_browser_type()
+ assert browser_type == 'edge'
+
+ # Test without binary location (should default to chrome)
+ manager.options.binary_location = ""
+ browser_type = manager._detect_browser_type()
+ assert browser_type == 'chrome'
\ No newline at end of file
From 513b40650e6f59bafcdaa73aafc0586f38b4989b Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 14:53:03 +0800
Subject: [PATCH 04/61] feat(test): add fingerprint spoofing integration tests
---
tests/test_fingerprint_integration.py | 491 ++++++++++++++++++++++++++
1 file changed, 491 insertions(+)
create mode 100644 tests/test_fingerprint_integration.py
diff --git a/tests/test_fingerprint_integration.py b/tests/test_fingerprint_integration.py
new file mode 100644
index 00000000..00952718
--- /dev/null
+++ b/tests/test_fingerprint_integration.py
@@ -0,0 +1,491 @@
+"""
+Tests for fingerprint spoofing integration functionality.
+
+This module contains integration tests for fingerprint spoofing features
+including browser integration, options manager, and error handling.
+"""
+
+import asyncio
+import json
+import tempfile
+from pathlib import Path
+from unittest.mock import Mock, patch, AsyncMock, MagicMock
+
+import pytest
+
+from pydoll.browser.chromium.chrome import Chrome
+from pydoll.browser.chromium.edge import Edge
+from pydoll.browser.managers.browser_options_manager import ChromiumOptionsManager
+from pydoll.browser.options import ChromiumOptions
+from pydoll.browser.tab import Tab
+from pydoll.fingerprint import (
+ FingerprintConfig,
+ FingerprintManager,
+ FingerprintGenerator,
+ Fingerprint,
+)
+from pydoll.exceptions import InvalidOptionsObject
+
+
+class TestFingerprintIntegration:
+ """Test fingerprint spoofing integration with browsers."""
+
+ @pytest.fixture
+ def mock_tab(self):
+ """Create a mock tab for testing."""
+ tab = Mock(spec=Tab)
+ tab._execute_command = AsyncMock()
+ tab.execute_script = AsyncMock()
+ return tab
+
+ @pytest.fixture
+ def test_fingerprint(self):
+ """Create a test fingerprint."""
+ return Fingerprint(
+ user_agent="Test User Agent",
+ platform="Win32",
+ language="en-US",
+ languages=["en-US", "en"],
+ hardware_concurrency=4,
+ screen_width=1920,
+ screen_height=1080,
+ screen_color_depth=24,
+ screen_pixel_depth=24,
+ available_width=1920,
+ available_height=1040,
+ viewport_width=1200,
+ viewport_height=800,
+ inner_width=1200,
+ inner_height=680,
+ webgl_vendor="Google Inc.",
+ webgl_renderer="Test Renderer",
+ webgl_version="WebGL 1.0",
+ webgl_shading_language_version="WebGL GLSL ES 1.0",
+ webgl_extensions=["EXT_test"],
+ canvas_fingerprint="test_canvas_123",
+ audio_context_sample_rate=44100.0,
+ audio_context_state="suspended",
+ audio_context_max_channel_count=2,
+ timezone="America/New_York",
+ timezone_offset=-300,
+ browser_type="chrome",
+ browser_version="120.0.0.0",
+ plugins=[],
+ )
+
+ @pytest.mark.asyncio
+ async def test_browser_fingerprint_injection_chrome(self, mock_tab, test_fingerprint):
+ """Test fingerprint script injection in Chrome browser."""
+ with patch('pydoll.browser.chromium.chrome.Chrome._get_default_binary_location'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._setup_user_dir'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._verify_browser_running'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._get_valid_tab_id'), \
+ patch('pydoll.browser.tab.Tab') as mock_tab_class:
+
+ # Setup mocks
+ mock_tab_class.return_value = mock_tab
+
+ # Create Chrome with fingerprint spoofing enabled
+ chrome = Chrome(enable_fingerprint_spoofing=True)
+
+ # Verify fingerprint manager is created
+ assert chrome.enable_fingerprint_spoofing is True
+ assert chrome.fingerprint_manager is not None
+
+ # Mock the fingerprint manager
+ chrome.fingerprint_manager.get_fingerprint_js = Mock(return_value="test_script")
+
+ # Test the injection method directly
+ await chrome._inject_fingerprint_script(mock_tab)
+
+ # Verify script injection was attempted
+ mock_tab._execute_command.assert_called()
+ mock_tab.execute_script.assert_called_with("test_script")
+
+ @pytest.mark.asyncio
+ async def test_browser_fingerprint_injection_edge(self, mock_tab):
+ """Test fingerprint script injection in Edge browser."""
+ with patch('pydoll.browser.chromium.edge.Edge._get_default_binary_location'), \
+ patch('pydoll.browser.chromium.edge.Edge._setup_user_dir'), \
+ patch('pydoll.browser.chromium.edge.Edge._verify_browser_running'), \
+ patch('pydoll.browser.chromium.edge.Edge._get_valid_tab_id'), \
+ patch('pydoll.browser.tab.Tab') as mock_tab_class:
+
+ # Setup mocks
+ mock_tab_class.return_value = mock_tab
+
+ # Create Edge with fingerprint spoofing enabled
+ edge = Edge(enable_fingerprint_spoofing=True)
+
+ # Verify fingerprint manager is created
+ assert edge.enable_fingerprint_spoofing is True
+ assert edge.fingerprint_manager is not None
+
+ # Mock the fingerprint manager
+ edge.fingerprint_manager.get_fingerprint_js = Mock(return_value="test_script")
+
+ # Test the injection method directly
+ await edge._inject_fingerprint_script(mock_tab)
+
+ # Verify script injection was attempted
+ mock_tab._execute_command.assert_called()
+ mock_tab.execute_script.assert_called_with("test_script")
+
+ @pytest.mark.asyncio
+ async def test_fingerprint_injection_with_script_error(self):
+ """Test fingerprint injection handles script execution errors gracefully."""
+ with patch('pydoll.browser.chromium.chrome.Chrome._get_default_binary_location'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._setup_user_dir'):
+
+ chrome = Chrome(enable_fingerprint_spoofing=True)
+
+ # Create mock tab
+ mock_tab = Mock()
+ mock_tab._execute_command = AsyncMock()
+ mock_tab.execute_script = AsyncMock()
+
+ # Mock fingerprint manager
+ if chrome.fingerprint_manager:
+ chrome.fingerprint_manager.get_fingerprint_js = Mock(return_value="test_script")
+
+ # Make execute_script raise an exception
+ mock_tab.execute_script.side_effect = Exception("Script execution failed")
+
+ # Should not raise exception - errors should be handled gracefully
+ await chrome._inject_fingerprint_script(mock_tab)
+
+ # Verify script injection was still attempted
+ mock_tab._execute_command.assert_called()
+
+ @pytest.mark.asyncio
+ async def test_fingerprint_injection_no_manager(self, mock_tab):
+ """Test fingerprint injection when no manager is available."""
+ with patch('pydoll.browser.chromium.chrome.Chrome._get_default_binary_location'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._setup_user_dir'):
+
+ chrome = Chrome(enable_fingerprint_spoofing=False)
+ chrome.fingerprint_manager = None
+
+ # Should handle gracefully when no fingerprint manager
+ await chrome._inject_fingerprint_script(mock_tab)
+
+ # No commands should be executed
+ mock_tab._execute_command.assert_not_called()
+ mock_tab.execute_script.assert_not_called()
+
+ def test_get_fingerprint_summary_with_manager(self):
+ """Test getting fingerprint summary when manager exists."""
+ with patch('pydoll.browser.chromium.chrome.Chrome._get_default_binary_location'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._setup_user_dir'):
+
+ chrome = Chrome(enable_fingerprint_spoofing=True)
+
+ # Mock fingerprint manager summary
+ expected_summary = {"browser": "Chrome", "version": "120.0.0.0"}
+ chrome.fingerprint_manager.get_fingerprint_summary = Mock(return_value=expected_summary)
+
+ summary = chrome.get_fingerprint_summary()
+ assert summary == expected_summary
+
+ def test_get_fingerprint_summary_no_manager(self):
+ """Test getting fingerprint summary when no manager exists."""
+ with patch('pydoll.browser.chromium.chrome.Chrome._get_default_binary_location'), \
+ patch('pydoll.browser.chromium.chrome.Chrome._setup_user_dir'):
+
+ chrome = Chrome(enable_fingerprint_spoofing=False)
+ chrome.fingerprint_manager = None
+
+ summary = chrome.get_fingerprint_summary()
+ assert summary is None
+
+
+class TestBrowserOptionsManagerFingerprinting:
+ """Test fingerprint spoofing integration with browser options manager."""
+
+ def test_options_manager_with_fingerprinting_enabled(self):
+ """Test options manager with fingerprint spoofing enabled."""
+ config = FingerprintConfig(browser_type="chrome", enable_webgl_spoofing=True)
+ manager = ChromiumOptionsManager(
+ enable_fingerprint_spoofing=True,
+ fingerprint_config=config
+ )
+
+ # Verify fingerprint manager is created
+ assert manager.enable_fingerprint_spoofing is True
+ assert manager.fingerprint_manager is not None
+ assert manager.fingerprint_config is not None
+
+ def test_options_manager_fingerprint_spoofing_disabled(self):
+ """Test options manager with fingerprint spoofing disabled."""
+ manager = ChromiumOptionsManager(enable_fingerprint_spoofing=False)
+
+ assert manager.enable_fingerprint_spoofing is False
+ assert manager.fingerprint_manager is None
+
+ def test_initialize_options_with_fingerprinting(self):
+ """Test options initialization with fingerprint spoofing."""
+ config = FingerprintConfig(browser_type="chrome")
+ manager = ChromiumOptionsManager(
+ enable_fingerprint_spoofing=True,
+ fingerprint_config=config
+ )
+
+ # Mock fingerprint manager methods
+ manager.fingerprint_manager.generate_new_fingerprint = Mock()
+ manager.fingerprint_manager.get_fingerprint_arguments = Mock(return_value=[
+ '--user-agent=Test Agent',
+ '--lang=en-US'
+ ])
+
+ options = manager.initialize_options()
+
+ # Verify options were initialized
+ assert isinstance(options, ChromiumOptions)
+
+ # Verify fingerprint methods were called
+ manager.fingerprint_manager.generate_new_fingerprint.assert_called_with('chrome')
+ manager.fingerprint_manager.get_fingerprint_arguments.assert_called_with('chrome')
+
+ def test_detect_browser_type_chrome(self):
+ """Test browser type detection for Chrome."""
+ manager = ChromiumOptionsManager(enable_fingerprint_spoofing=True)
+ manager.options = ChromiumOptions()
+ manager.options.binary_location = "/path/to/chrome"
+
+ browser_type = manager._detect_browser_type()
+ assert browser_type == 'chrome'
+
+ def test_detect_browser_type_edge(self):
+ """Test browser type detection for Edge."""
+ manager = ChromiumOptionsManager(enable_fingerprint_spoofing=True)
+ manager.options = ChromiumOptions()
+ manager.options.binary_location = "/path/to/msedge"
+
+ browser_type = manager._detect_browser_type()
+ assert browser_type == 'edge'
+
+ def test_detect_browser_type_default(self):
+ """Test browser type detection defaults to chrome."""
+ manager = ChromiumOptionsManager(enable_fingerprint_spoofing=True)
+ manager.options = ChromiumOptions()
+ manager.options.binary_location = None
+
+ browser_type = manager._detect_browser_type()
+ assert browser_type == 'chrome'
+
+ def test_get_fingerprint_manager(self):
+ """Test getting fingerprint manager instance."""
+ manager = ChromiumOptionsManager(enable_fingerprint_spoofing=True)
+
+ fingerprint_manager = manager.get_fingerprint_manager()
+ assert fingerprint_manager is not None
+ assert fingerprint_manager == manager.fingerprint_manager
+
+ def test_get_fingerprint_manager_disabled(self):
+ """Test getting fingerprint manager when disabled."""
+ manager = ChromiumOptionsManager(enable_fingerprint_spoofing=False)
+
+ fingerprint_manager = manager.get_fingerprint_manager()
+ assert fingerprint_manager is None
+
+ def test_invalid_options_object(self):
+ """Test error handling for invalid options object."""
+ manager = ChromiumOptionsManager()
+ manager.options = Mock() # Invalid options object
+
+ with pytest.raises(InvalidOptionsObject):
+ manager.initialize_options()
+
+
+class TestFingerprintManagerErrorHandling:
+ """Test error handling in fingerprint manager."""
+
+ def test_get_fingerprint_js_without_injector(self):
+ """Test error when getting JS without generated fingerprint."""
+ manager = FingerprintManager()
+
+ with pytest.raises(ValueError, match="No fingerprint has been generated"):
+ manager.get_fingerprint_js()
+
+ def test_get_fingerprint_arguments_without_fingerprint(self):
+ """Test error when getting arguments without generated fingerprint."""
+ manager = FingerprintManager()
+
+ with pytest.raises(ValueError, match="No fingerprint has been generated"):
+ manager.get_fingerprint_arguments()
+
+ def test_save_fingerprint_without_fingerprint(self):
+ """Test error when saving without current fingerprint."""
+ manager = FingerprintManager()
+
+ with pytest.raises(ValueError, match="No fingerprint provided"):
+ manager.save_fingerprint("test")
+
+ def test_get_fingerprint_summary_without_fingerprint(self):
+ """Test error when getting summary without fingerprint."""
+ manager = FingerprintManager()
+
+ with pytest.raises(ValueError, match="No fingerprint provided"):
+ manager.get_fingerprint_summary()
+
+ def test_load_nonexistent_fingerprint(self):
+ """Test error when loading non-existent fingerprint."""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ config = FingerprintConfig()
+ manager = FingerprintManager(config)
+ manager.storage_dir = Path(temp_dir)
+
+ with pytest.raises(FileNotFoundError, match="Fingerprint 'nonexistent' not found"):
+ manager.load_fingerprint("nonexistent")
+
+ def test_load_invalid_fingerprint_file(self):
+ """Test error when loading invalid fingerprint file."""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ config = FingerprintConfig()
+ manager = FingerprintManager(config)
+ manager.storage_dir = Path(temp_dir)
+
+ # Create invalid JSON file
+ invalid_file = Path(temp_dir) / "invalid.json"
+ with open(invalid_file, 'w') as f:
+ f.write("invalid json content")
+
+ with pytest.raises(ValueError, match="Invalid fingerprint file"):
+ manager.load_fingerprint("invalid")
+
+
+class TestFingerprintManagerFileOperations:
+ """Test file operations in fingerprint manager."""
+
+ def test_save_and_load_fingerprint(self):
+ """Test saving and loading fingerprint functionality."""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ config = FingerprintConfig()
+ manager = FingerprintManager(config)
+ manager.storage_dir = Path(temp_dir)
+
+ # Generate a fingerprint
+ fingerprint = manager.generate_new_fingerprint()
+
+ # Save fingerprint
+ saved_path = manager.save_fingerprint("test_fp")
+ assert Path(saved_path).exists()
+
+ # Clear current fingerprint
+ manager.clear_current_fingerprint()
+ assert manager.current_fingerprint is None
+
+ # Load fingerprint
+ loaded_fp = manager.load_fingerprint("test_fp")
+ assert loaded_fp.user_agent == fingerprint.user_agent
+ assert loaded_fp.browser_type == fingerprint.browser_type
+
+ def test_list_saved_fingerprints(self):
+ """Test listing saved fingerprints."""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ config = FingerprintConfig()
+ manager = FingerprintManager(config)
+ manager.storage_dir = Path(temp_dir)
+
+ # Initially no fingerprints
+ assert manager.list_saved_fingerprints() == []
+
+ # Generate and save fingerprints
+ manager.generate_new_fingerprint()
+ manager.save_fingerprint("fp1")
+
+ manager.generate_new_fingerprint(force=True)
+ manager.save_fingerprint("fp2")
+
+ # List should contain both
+ saved_fps = manager.list_saved_fingerprints()
+ assert len(saved_fps) == 2
+ assert "fp1" in saved_fps
+ assert "fp2" in saved_fps
+
+ def test_delete_fingerprint(self):
+ """Test deleting saved fingerprint."""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ config = FingerprintConfig()
+ manager = FingerprintManager(config)
+ manager.storage_dir = Path(temp_dir)
+
+ # Generate and save fingerprint
+ manager.generate_new_fingerprint()
+ manager.save_fingerprint("to_delete")
+
+ # Verify it exists
+ assert "to_delete" in manager.list_saved_fingerprints()
+
+ # Delete it
+ result = manager.delete_fingerprint("to_delete")
+ assert result is True
+
+ # Verify it's gone
+ assert "to_delete" not in manager.list_saved_fingerprints()
+
+ def test_delete_nonexistent_fingerprint(self):
+ """Test deleting non-existent fingerprint."""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ config = FingerprintConfig()
+ manager = FingerprintManager(config)
+ manager.storage_dir = Path(temp_dir)
+
+ # Try to delete non-existent fingerprint
+ result = manager.delete_fingerprint("nonexistent")
+ assert result is False
+
+ def test_get_current_fingerprint(self):
+ """Test getting current fingerprint."""
+ manager = FingerprintManager()
+
+ # Initially None
+ assert manager.get_current_fingerprint() is None
+
+ # After generation
+ fingerprint = manager.generate_new_fingerprint()
+ current = manager.get_current_fingerprint()
+ assert current is not None
+ assert current == fingerprint
+
+ def test_clear_current_fingerprint(self):
+ """Test clearing current fingerprint."""
+ manager = FingerprintManager()
+
+ # Generate fingerprint
+ manager.generate_new_fingerprint()
+ assert manager.current_fingerprint is not None
+ assert manager.injector is not None
+
+ # Clear it
+ manager.clear_current_fingerprint()
+ assert manager.current_fingerprint is None
+ assert manager.injector is None
+
+
+class TestFingerprintGeneratorEdgeCases:
+ """Test edge cases in fingerprint generator."""
+
+ def test_generate_unique_properties(self):
+ """Test unique properties generation."""
+ from pydoll.fingerprint.generator import FingerprintGenerator
+
+ props = FingerprintGenerator._generate_unique_properties()
+ assert "unique_id" in props
+ assert isinstance(props["unique_id"], str)
+ assert len(props["unique_id"]) > 0
+
+ def test_generate_plugins(self):
+ """Test plugins generation."""
+ config = FingerprintConfig()
+ generator = FingerprintGenerator(config)
+
+ plugins = generator._generate_plugins()
+ assert isinstance(plugins, list)
+ assert len(plugins) > 0
+
+ # Check plugin structure
+ for plugin in plugins:
+ assert "name" in plugin
+ assert "filename" in plugin
+ assert "description" in plugin
\ No newline at end of file
From 969649c005106d20c4689d2c8b68a1e1870c17a8 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 14:53:21 +0800
Subject: [PATCH 05/61] feat(test): add fingerprint refactoring verification
tests
---
tests/test_refactor.py | 108 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 108 insertions(+)
create mode 100644 tests/test_refactor.py
diff --git a/tests/test_refactor.py b/tests/test_refactor.py
new file mode 100644
index 00000000..ccb71375
--- /dev/null
+++ b/tests/test_refactor.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+"""
+Test file to verify fingerprint spoofing functionality after refactoring
+"""
+
+def test_imports():
+ """Test if all imports work properly"""
+ print("=== Testing Imports ===")
+
+ # Test browser class imports
+ from pydoll.browser.chromium.chrome import Chrome
+ from pydoll.browser.chromium.edge import Edge
+ print("[PASS] Browser classes imported successfully")
+
+ # Test fingerprint configuration imports
+ from pydoll.fingerprint import FingerprintConfig, FingerprintManager
+ print("[PASS] Fingerprint configuration imported successfully")
+
+ # Test if old imports have been removed
+ try:
+ from pydoll.fingerprint.browser_options import FingerprintBrowserOptionsManager
+ print("[FAIL] Error: Old FingerprintBrowserOptionsManager still exists")
+ assert False, "Old FingerprintBrowserOptionsManager still exists"
+ except ImportError:
+ print("[PASS] Old FingerprintBrowserOptionsManager correctly removed")
+
+def test_chrome_initialization():
+ """Test Chrome browser initialization"""
+ print("\n=== Testing Chrome Browser ===")
+
+ from pydoll.browser.chromium.chrome import Chrome
+ from pydoll.fingerprint import FingerprintConfig
+
+ # Test basic initialization
+ chrome = Chrome()
+ print(f"[PASS] Chrome basic initialization: fingerprint_spoofing={chrome.enable_fingerprint_spoofing}")
+ assert chrome.enable_fingerprint_spoofing == False
+
+ # Test with fingerprint spoofing enabled
+ chrome_fp = Chrome(enable_fingerprint_spoofing=True)
+ print(f"[PASS] Chrome fingerprint spoofing: enabled={chrome_fp.enable_fingerprint_spoofing}, manager={chrome_fp.fingerprint_manager is not None}")
+ assert chrome_fp.enable_fingerprint_spoofing == True
+ assert chrome_fp.fingerprint_manager is not None
+
+ # Test custom configuration
+ config = FingerprintConfig(browser_type="chrome", enable_webgl_spoofing=True)
+ chrome_custom = Chrome(enable_fingerprint_spoofing=True, fingerprint_config=config)
+ print(f"[PASS] Chrome custom configuration: enabled={chrome_custom.enable_fingerprint_spoofing}")
+ assert chrome_custom.enable_fingerprint_spoofing == True
+
+def test_edge_initialization():
+ """Test Edge browser initialization"""
+ print("\n=== Testing Edge Browser ===")
+
+ from pydoll.browser.chromium.edge import Edge
+ from pydoll.fingerprint import FingerprintConfig
+
+ # Test basic initialization
+ edge = Edge()
+ print(f"[PASS] Edge basic initialization: fingerprint_spoofing={edge.enable_fingerprint_spoofing}")
+ assert edge.enable_fingerprint_spoofing == False
+
+ # Test with fingerprint spoofing enabled
+ edge_fp = Edge(enable_fingerprint_spoofing=True)
+ print(f"[PASS] Edge fingerprint spoofing: enabled={edge_fp.enable_fingerprint_spoofing}, manager={edge_fp.fingerprint_manager is not None}")
+ assert edge_fp.enable_fingerprint_spoofing == True
+ assert edge_fp.fingerprint_manager is not None
+
+def test_options_manager():
+ """Test options manager"""
+ print("\n=== Testing Options Manager ===")
+
+ from pydoll.browser.managers.browser_options_manager import ChromiumOptionsManager
+ from pydoll.fingerprint import FingerprintConfig
+
+ # Test basic manager
+ manager = ChromiumOptionsManager()
+ print(f"[PASS] Basic manager: fingerprint_spoofing={getattr(manager, 'enable_fingerprint_spoofing', False)}")
+ assert getattr(manager, 'enable_fingerprint_spoofing', False) == False
+
+ # Test manager with fingerprint spoofing enabled
+ config = FingerprintConfig(browser_type="chrome", enable_webgl_spoofing=True)
+ manager_fp = ChromiumOptionsManager(enable_fingerprint_spoofing=True, fingerprint_config=config)
+ print(f"[PASS] Fingerprint manager: enabled={manager_fp.enable_fingerprint_spoofing}, manager={manager_fp.fingerprint_manager is not None}")
+ assert manager_fp.enable_fingerprint_spoofing == True
+ assert manager_fp.fingerprint_manager is not None
+
+ # Test options initialization
+ options = manager_fp.initialize_options()
+ print(f"[PASS] Options initialization: number of arguments={len(options.arguments)}")
+ assert len(options.arguments) >= 2 # Should have at least default arguments
+
+def main():
+ """Run all tests"""
+ print("[INFO] Refactoring Verification Tests")
+ print("=" * 50)
+
+ # Run tests directly, no need to collect return values
+ test_imports()
+ test_chrome_initialization()
+ test_edge_initialization()
+ test_options_manager()
+
+ print("\n" + "=" * 50)
+ print("[SUCCESS] All tests passed! Refactoring successful!")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
From 481c1680545d490a8b28dab50bfeabdb12562bd4 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 14:56:09 +0800
Subject: [PATCH 06/61] feat(examples): add comprehensive fingerprint spoofing
example
Add detailed fingerprint_example.py demonstrating fingerprint spoofing functionality with various browser configurations and use cases.
This provides users with a complete 349-line guide on implementing fingerprint spoofing in their automation projects, covering Chrome/Edge setup, custom configurations, and integration workflows.
---
examples/fingerprint_example.py | 348 ++++++++++++++++++++++++++++++++
1 file changed, 348 insertions(+)
create mode 100644 examples/fingerprint_example.py
diff --git a/examples/fingerprint_example.py b/examples/fingerprint_example.py
new file mode 100644
index 00000000..8a509e62
--- /dev/null
+++ b/examples/fingerprint_example.py
@@ -0,0 +1,348 @@
+"""
+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.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
+ browser = Chrome(enable_fingerprint_spoofing=True)
+
+ 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
+ browser = Chrome(
+ enable_fingerprint_spoofing=True,
+ fingerprint_config=config
+ )
+
+ async with browser:
+ tab = await browser.start()
+
+ # Visit browser information detection website
+ await tab.go_to("https://browserleaks.com/javascript")
+
+ await asyncio.sleep(5)
+
+ # Get and print some browser information
+ try:
+ user_agent = await tab.execute_script("return navigator.userAgent")
+ print(f"User Agent: {user_agent}")
+
+ platform = await tab.execute_script("return navigator.platform")
+ print(f"Platform: {platform}")
+
+ screen_info = await tab.execute_script("""
+ return {
+ width: screen.width,
+ height: screen.height,
+ colorDepth: screen.colorDepth
+ }
+ """)
+ print(f"Screen info: {screen_info}")
+ 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
+ browser1 = Chrome(
+ enable_fingerprint_spoofing=True,
+ fingerprint_config=FingerprintConfig(browser_type="chrome")
+ )
+
+ async with browser1:
+ tab = await browser1.start()
+ await tab.go_to("https://amiunique.org/fingerprint")
+ 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}")
+
+ # 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
+ # Note: Chrome class accepts fingerprint_config instead of fingerprint_manager
+ browser1 = Chrome(
+ enable_fingerprint_spoofing=True,
+ fingerprint_config=FingerprintConfig(browser_type="chrome")
+ )
+ browser2 = Chrome(
+ enable_fingerprint_spoofing=True,
+ fingerprint_config=FingerprintConfig(browser_type="chrome")
+ )
+
+ 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
+ browser = Edge(enable_fingerprint_spoofing=True)
+
+ async with browser:
+ tab = await browser.start()
+
+ await tab.go_to("https://www.whatismybrowser.com/")
+ 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}")
+ except Exception as e:
+ print(f"Failed to get browser information: {e}")
+
+ await asyncio.sleep(3)
+
+
+async def main():
+ """Run all examples"""
+ try:
+ # Run multiple browsers example to test fingerprint uniqueness
+ await multiple_browsers_example()
+
+ # The following examples are temporarily commented out
+ # await basic_example()
+ # await custom_config_example()
+ # await persistent_fingerprint_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())
From 841f1d8144aaeeb4f8c1ec064c3c1189ad70765a Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 15:02:13 +0800
Subject: [PATCH 07/61] feat(constants): add comprehensive fingerprint spoofing
JavaScript constants
Add extensive fingerprint spoofing capabilities through JavaScript injection constants for browser automation stealth and anti-detection functionality.
---
pydoll/constants.py | 365 +++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 361 insertions(+), 4 deletions(-)
diff --git a/pydoll/constants.py b/pydoll/constants.py
index f70619a9..ab409717 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;
+ }});
+ }};
+ }}
+ """
+
class Key(tuple[str, int], Enum):
BACKSPACE = ('Backspace', 8)
From 572d5d7a64a1dc74e23c04a84d9cb64aeb3c6c80 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 15:03:27 +0800
Subject: [PATCH 08/61] feat(fingerprint): add fingerprint module
initialization and exports
Add module initialization file to expose fingerprint spoofing components with clean public API and global manager instance.
---
pydoll/fingerprint/__init__.py | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
create mode 100644 pydoll/fingerprint/__init__.py
diff --git a/pydoll/fingerprint/__init__.py b/pydoll/fingerprint/__init__.py
new file mode 100644
index 00000000..e2585eb4
--- /dev/null
+++ b/pydoll/fingerprint/__init__.py
@@ -0,0 +1,23 @@
+"""
+Browser fingerprint spoofing module for pydoll.
+
+This module provides comprehensive browser fingerprinting protection by generating
+random but realistic browser fingerprints and injecting them into the browser.
+"""
+
+from .generator import FingerprintGenerator
+from .injector import FingerprintInjector
+from .manager import FingerprintManager
+from .models import Fingerprint, FingerprintConfig
+
+# Global fingerprint manager instance
+FINGERPRINT_MANAGER = FingerprintManager()
+
+__all__ = [
+ 'FingerprintGenerator',
+ 'FingerprintInjector',
+ 'FingerprintManager',
+ 'Fingerprint',
+ 'FingerprintConfig',
+ 'FINGERPRINT_MANAGER',
+]
From cca581216a1a4932ac69cd81c8f3c466f17dfd90 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 15:03:56 +0800
Subject: [PATCH 09/61] feat(fingerprint): add sophisticated fingerprint
generation engine
Add advanced fingerprint generation system that creates realistic,randomized browser fingerprints for stealth automation.
---
pydoll/fingerprint/generator.py | 496 ++++++++++++++++++++++++++++++++
1 file changed, 496 insertions(+)
create mode 100644 pydoll/fingerprint/generator.py
diff --git a/pydoll/fingerprint/generator.py b/pydoll/fingerprint/generator.py
new file mode 100644
index 00000000..260156d3
--- /dev/null
+++ b/pydoll/fingerprint/generator.py
@@ -0,0 +1,496 @@
+"""
+Browser fingerprint generator.
+
+This module provides the FingerprintGenerator class which creates realistic
+browser fingerprints with randomized but consistent properties.
+"""
+
+import random
+import string
+import time
+import uuid
+from typing import Dict, List, Optional, Tuple
+
+from .models import Fingerprint, FingerprintConfig
+
+# Constants for magic values
+DEVICE_MEMORY_THRESHOLD = 0.3
+DO_NOT_TRACK_THRESHOLD = 0.8
+
+
+class FingerprintGenerator:
+ """
+ Generates realistic browser fingerprints for spoofing purposes.
+
+ This class creates fingerprints that appear authentic to fingerprinting
+ detection systems by using realistic combinations of browser properties.
+ """
+
+ # Chrome versions (recent stable versions)
+ CHROME_VERSIONS = [
+ '120.0.6099.129', '121.0.6167.139', '122.0.6261.69',
+ '123.0.6312.86', '124.0.6367.62', '125.0.6422.76',
+ '126.0.6478.55', '127.0.6533.110', '128.0.6613.120',
+ '129.0.6668.100', '130.0.6723.70', '131.0.6778.85'
+ ]
+
+ # Edge versions
+ EDGE_VERSIONS = [
+ '120.0.2210.89', '121.0.2277.98', '122.0.2365.59',
+ '123.0.2420.53', '124.0.2478.49', '125.0.2535.33',
+ '126.0.2592.68', '127.0.2651.105', '128.0.2739.67',
+ '129.0.2792.52', '130.0.2849.68', '131.0.2903.70'
+ ]
+
+ # Operating systems with realistic distributions
+ OPERATING_SYSTEMS = [
+ {'name': 'Windows', 'version': '10.0', 'platform': 'Win32'},
+ {'name': 'Windows', 'version': '11.0', 'platform': 'Win32'},
+ {'name': 'Macintosh', 'version': '10.15.7', 'platform': 'MacIntel'},
+ {'name': 'Macintosh', 'version': '11.6.8', 'platform': 'MacIntel'},
+ {'name': 'Macintosh', 'version': '12.6.0', 'platform': 'MacIntel'},
+ {'name': 'Linux', 'version': 'x86_64', 'platform': 'Linux x86_64'},
+ ]
+
+ # Common screen resolutions
+ SCREEN_RESOLUTIONS = [
+ (1920, 1080), (1366, 768), (1440, 900), (1536, 864),
+ (1280, 720), (1600, 900), (2560, 1440), (1920, 1200),
+ (1680, 1050), (1280, 1024), (2048, 1152), (1280, 800)
+ ]
+
+ # Mobile screen resolutions (smaller screens)
+ MOBILE_SCREEN_RESOLUTIONS = [
+ (375, 667), # iPhone 8
+ (414, 896), # iPhone XR
+ (390, 844), # iPhone 12
+ (360, 800), # Samsung Galaxy S10
+ (412, 915), # Samsung Galaxy S21
+ (393, 851), # Google Pixel 5
+ (360, 640), # Common Android
+ (414, 736), # iPhone 8 Plus
+ (428, 926), # iPhone 13 Pro Max
+ ]
+
+ # Language preferences
+ LANGUAGES = [
+ 'en-US,en;q=0.9',
+ 'en-GB,en;q=0.9',
+ 'zh-CN,zh;q=0.9,en;q=0.8',
+ 'ja-JP,ja;q=0.9,en;q=0.8',
+ 'es-ES,es;q=0.9,en;q=0.8',
+ 'fr-FR,fr;q=0.9,en;q=0.8',
+ 'de-DE,de;q=0.9,en;q=0.8',
+ 'ru-RU,ru;q=0.9,en;q=0.8',
+ 'pt-BR,pt;q=0.9,en;q=0.8',
+ 'it-IT,it;q=0.9,en;q=0.8',
+ 'ko-KR,ko;q=0.9,en;q=0.8',
+ ]
+
+ # WebGL vendors and renderers
+ WEBGL_VENDORS = [
+ 'Google Inc. (NVIDIA)',
+ 'Google Inc. (Intel)',
+ 'Google Inc. (AMD)',
+ 'Microsoft Corporation (NVIDIA)',
+ 'Microsoft Corporation (Intel)',
+ 'Microsoft Corporation (AMD)',
+ 'NVIDIA Corporation',
+ 'Intel Inc.',
+ 'AMD Inc.',
+ ]
+
+ WEBGL_RENDERERS = [
+ 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 Direct3D11 vs_5_0 ps_5_0, D3D11)',
+ 'ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11)',
+ 'ANGLE (AMD, AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0, D3D11)',
+ 'WebKit WebGL',
+ 'Mozilla',
+ 'ANGLE (NVIDIA Corporation, NVIDIA GeForce RTX 3070 Direct3D11 vs_5_0 ps_5_0)',
+ 'ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)',
+ ]
+
+ # Common WebGL extensions
+ WEBGL_EXTENSIONS = [
+ 'ANGLE_instanced_arrays', 'EXT_blend_minmax', 'EXT_color_buffer_half_float',
+ 'EXT_disjoint_timer_query', 'EXT_float_blend', 'EXT_frag_depth',
+ 'EXT_shader_texture_lod', 'EXT_texture_compression_bptc', 'EXT_texture_compression_rgtc',
+ 'EXT_texture_filter_anisotropic', 'WEBKIT_EXT_texture_filter_anisotropic',
+ 'EXT_sRGB', 'OES_element_index_uint', 'OES_fbo_render_mipmap',
+ 'OES_standard_derivatives', 'OES_texture_float', 'OES_texture_float_linear',
+ 'OES_texture_half_float', 'OES_texture_half_float_linear', 'OES_vertex_array_object',
+ 'WEBGL_color_buffer_float', 'WEBGL_compressed_texture_s3tc',
+ 'WEBKIT_WEBGL_compressed_texture_s3tc', 'WEBGL_compressed_texture_s3tc_srgb',
+ 'WEBGL_debug_renderer_info', 'WEBGL_debug_shaders', 'WEBGL_depth_texture',
+ 'WEBKIT_WEBGL_depth_texture', 'WEBGL_draw_buffers', 'WEBGL_lose_context',
+ 'WEBKIT_WEBGL_lose_context'
+ ]
+
+ # Audio context sample rates
+ AUDIO_SAMPLE_RATES = [44100, 48000, 96000]
+
+ # Common timezones
+ TIMEZONES = [
+ 'America/New_York', 'America/Los_Angeles', 'America/Chicago',
+ 'Europe/London', 'Europe/Paris', 'Europe/Berlin',
+ 'Asia/Tokyo', 'Asia/Shanghai', 'Asia/Seoul',
+ 'Australia/Sydney', 'America/Toronto', 'Europe/Madrid'
+ ]
+
+ def __init__(self, config: Optional[FingerprintConfig] = None):
+ """
+ Initialize the fingerprint generator.
+
+ Args:
+ config: Configuration for fingerprint generation. Uses default if None.
+ """
+ self.config = config or FingerprintConfig()
+ # 使ēØå½åę¶é“åéęŗUUIDåå§åéęŗē§åļ¼ē”®äæęÆę¬”ēęēęēŗ¹é½äøå
+ random.seed(time.time() + hash(str(uuid.uuid4())))
+
+ def generate(self) -> Fingerprint:
+ """
+ Generate a complete browser fingerprint.
+
+ Returns:
+ A Fingerprint object with all properties set.
+ """
+ # Generate basic system properties
+ system_properties = self._generate_system_properties()
+
+ # Generate display properties
+ display_properties = self._generate_display_properties()
+
+ # Generate browser properties
+ browser_properties = self._generate_browser_properties()
+
+ # Generate multimedia properties
+ multimedia_properties = self._generate_multimedia_properties()
+
+ # ę·»å äøäŗå¾®å°ēéęŗååļ¼ē”®äæęÆę¬”ēęēęēŗ¹é½äøå
+ unique_properties = self._generate_unique_properties()
+
+ # Combine all properties into fingerprint
+ return Fingerprint(**{
+ **system_properties,
+ **display_properties,
+ **browser_properties,
+ **multimedia_properties,
+ **unique_properties,
+ })
+
+ def _generate_system_properties(self) -> Dict:
+ """Generate system-related properties."""
+ os_info = self._choose_operating_system()
+ browser_version = self._choose_browser_version()
+ language = random.choice(self.LANGUAGES)
+ timezone = random.choice(self.TIMEZONES)
+
+ return {
+ 'user_agent': self._generate_user_agent(os_info, browser_version),
+ 'platform': os_info['platform'],
+ 'language': language.split(',')[0],
+ 'languages': self._generate_language_list(language),
+ 'hardware_concurrency': random.choice([2, 4, 6, 8, 12, 16]),
+ 'device_memory': (
+ random.choice([2, 4, 8, 16])
+ if random.random() > DEVICE_MEMORY_THRESHOLD
+ else None
+ ),
+ 'timezone': timezone,
+ 'timezone_offset': self._get_timezone_offset(timezone),
+ 'browser_type': self.config.browser_type,
+ 'browser_version': browser_version,
+ 'chrome_version': browser_version if self.config.browser_type == 'chrome' else None,
+ 'cookie_enabled': True,
+ 'do_not_track': (
+ random.choice([None, '1'])
+ if random.random() > DO_NOT_TRACK_THRESHOLD
+ else None
+ ),
+ 'webdriver': False,
+ 'connection_type': random.choice(['wifi', 'ethernet', 'cellular']),
+ }
+
+ def _generate_display_properties(self) -> Dict:
+ """Generate display and viewport properties."""
+ screen_width, screen_height = self._choose_screen_resolution()
+ viewport_width = screen_width - random.randint(0, 100)
+ viewport_height = screen_height - random.randint(100, 200)
+
+ return {
+ 'screen_width': screen_width,
+ 'screen_height': screen_height,
+ 'screen_color_depth': 24,
+ 'screen_pixel_depth': 24,
+ 'available_width': screen_width,
+ 'available_height': screen_height - 40, # Account for taskbar
+ 'viewport_width': viewport_width,
+ 'viewport_height': viewport_height,
+ 'inner_width': viewport_width,
+ 'inner_height': viewport_height - 120, # Account for browser UI
+ }
+
+ def _generate_browser_properties(self) -> Dict:
+ """Generate browser-specific properties."""
+ webgl_vendor, webgl_renderer = self._choose_webgl_properties()
+
+ return {
+ 'webgl_vendor': webgl_vendor,
+ 'webgl_renderer': webgl_renderer,
+ 'webgl_version': 'WebGL 1.0 (OpenGL ES 2.0 Chromium)',
+ 'webgl_shading_language_version': 'WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)',
+ 'webgl_extensions': random.sample(self.WEBGL_EXTENSIONS, random.randint(15, 25)),
+ 'canvas_fingerprint': self._generate_canvas_fingerprint(),
+ 'plugins': self._generate_plugins() if self.config.include_plugins else [],
+ }
+
+ def _generate_multimedia_properties(self) -> Dict:
+ """Generate multimedia-related properties."""
+ return {
+ 'audio_context_sample_rate': float(random.choice(self.AUDIO_SAMPLE_RATES)),
+ 'audio_context_state': 'suspended',
+ 'audio_context_max_channel_count': random.choice([2, 6, 8]),
+ }
+
+ def _choose_operating_system(self) -> Dict[str, str]:
+ """Choose a random operating system."""
+ if self.config.preferred_os:
+ # Convert to lowercase for case-insensitive comparison
+ preferred_os_lower = self.config.preferred_os.lower()
+
+ # Map common OS names to their actual names in our data
+ os_mapping = {
+ 'windows': 'Windows',
+ 'win': 'Windows',
+ 'macos': 'Macintosh',
+ 'mac': 'Macintosh',
+ 'osx': 'Macintosh',
+ 'linux': 'Linux'
+ }
+
+ # Get the standardized OS name
+ target_os = os_mapping.get(preferred_os_lower)
+
+ if target_os:
+ filtered_os = [os for os in self.OPERATING_SYSTEMS
+ if os['name'] == target_os]
+ if filtered_os:
+ return random.choice(filtered_os)
+
+ # If no preferred OS or no match found, return random OS
+ return random.choice(self.OPERATING_SYSTEMS)
+
+ def _choose_browser_version(self) -> str:
+ """Choose a random browser version."""
+ if self.config.browser_type == 'edge':
+ return random.choice(self.EDGE_VERSIONS)
+ else:
+ return random.choice(self.CHROME_VERSIONS)
+
+ def _choose_screen_resolution(self) -> Tuple[int, int]:
+ """Choose a screen resolution within configured bounds."""
+ # Use mobile resolutions if is_mobile is set
+ if self.config.is_mobile:
+ valid_resolutions = [
+ (w, h) for w, h in self.MOBILE_SCREEN_RESOLUTIONS
+ if self.config.min_screen_width <= w <= self.config.max_screen_width
+ and self.config.min_screen_height <= h <= self.config.max_screen_height
+ ]
+ else:
+ valid_resolutions = [
+ (w, h) for w, h in self.SCREEN_RESOLUTIONS
+ if self.config.min_screen_width <= w <= self.config.max_screen_width
+ and self.config.min_screen_height <= h <= self.config.max_screen_height
+ ]
+
+ if not valid_resolutions:
+ # Fallback to a default resolution
+ return (1920, 1080) if not self.config.is_mobile else (375, 667)
+
+ return random.choice(valid_resolutions)
+
+ def _choose_webgl_properties(self) -> Tuple[str, str]:
+ """Choose WebGL vendor and renderer."""
+ vendor = random.choice(self.WEBGL_VENDORS)
+ renderer = random.choice(self.WEBGL_RENDERERS)
+ return vendor, renderer
+
+ def _generate_user_agent(self, os_info: Dict[str, str], browser_version: str) -> str:
+ """Generate a realistic user agent string."""
+ os_name = os_info['name']
+ os_version = os_info['version']
+
+ # Handle mobile user agents
+ if self.config.is_mobile:
+ return self._generate_mobile_user_agent(os_name, browser_version)
+
+ # Handle desktop user agents
+ if self.config.browser_type == 'chrome':
+ return self._generate_chrome_user_agent(os_name, os_version, browser_version)
+ elif self.config.browser_type == 'edge':
+ return self._generate_edge_user_agent(os_name, os_version, browser_version)
+ else:
+ # Fallback to Chrome
+ return self._generate_chrome_user_agent('Windows', '10.0', browser_version)
+
+ @staticmethod
+ def _generate_mobile_user_agent(os_name: str, browser_version: str) -> str:
+ """Generate a mobile user agent string."""
+ if random.choice([True, False]): # Android
+ android_version = random.randint(10, 13)
+ device_models = [
+ "SM-G998B", "Pixel 6", "Pixel 7", "OnePlus 10 Pro",
+ "Redmi Note 11", "Moto G Power"
+ ]
+ device = random.choice(device_models)
+ return (
+ f"Mozilla/5.0 (Linux; Android {android_version}; {device}) "
+ f"AppleWebKit/537.36 (KHTML, like Gecko) "
+ f"Chrome/{browser_version} Mobile Safari/537.36"
+ )
+ else: # iOS
+ ios_version = f"{random.randint(14, 16)}_{random.randint(0, 6)}"
+ device_models = ["iPhone", "iPad"]
+ device = random.choice(device_models)
+ webkit_version = f"60{random.randint(1, 9)}.{random.randint(1, 9)}"
+ return (
+ f"Mozilla/5.0 ({device}; CPU OS {ios_version} like Mac OS X) "
+ f"AppleWebKit/{webkit_version}.{random.randint(10, 99)} (KHTML, like Gecko) "
+ f"CriOS/{browser_version} Mobile/15E148 Safari/{webkit_version}"
+ )
+
+ @staticmethod
+ def _generate_chrome_user_agent(
+ os_name: str, os_version: str, browser_version: str
+ ) -> str:
+ """Generate Chrome user agent for specific OS."""
+ base_template = (
+ 'Mozilla/5.0 ({os_part}) AppleWebKit/537.36 '
+ '(KHTML, like Gecko) Chrome/{version} Safari/537.36'
+ )
+
+ os_parts = {
+ 'Windows': f'Windows NT {os_version}; Win64; x64',
+ 'Macintosh': f'Macintosh; Intel Mac OS X {os_version.replace(".", "_")}',
+ 'Linux': 'X11; Linux x86_64'
+ }
+
+ os_part = os_parts.get(os_name, os_parts['Windows'])
+ return base_template.format(os_part=os_part, version=browser_version)
+
+ @staticmethod
+ def _generate_edge_user_agent(
+ os_name: str, os_version: str, browser_version: str
+ ) -> str:
+ """Generate Edge user agent for specific OS."""
+ chrome_major = browser_version.split(".")[0]
+ base_template = (
+ 'Mozilla/5.0 ({os_part}) AppleWebKit/537.36 '
+ '(KHTML, like Gecko) Chrome/{chrome_major}.0.0.0 '
+ 'Safari/537.36 Edg/{version}'
+ )
+
+ os_parts = {
+ 'Windows': f'Windows NT {os_version}; Win64; x64',
+ 'Macintosh': f'Macintosh; Intel Mac OS X {os_version.replace(".", "_")}',
+ 'Linux': 'X11; Linux x86_64'
+ }
+
+ os_part = os_parts.get(os_name, os_parts['Windows'])
+ return base_template.format(
+ os_part=os_part, chrome_major=chrome_major, version=browser_version
+ )
+
+ @staticmethod
+ def _generate_language_list(primary_language: str) -> List[str]:
+ """Generate a list of languages based on the primary language."""
+ base_lang = primary_language.split(',')[0]
+ lang_code = base_lang.split('-')[0]
+
+ languages = [base_lang]
+
+ # Add the base language code if different
+ if lang_code != base_lang:
+ languages.append(lang_code)
+
+ # Add English as fallback for non-English primary languages
+ if not base_lang.startswith('en'):
+ languages.extend(['en-US', 'en'])
+
+ return languages
+
+ @staticmethod
+ def _get_timezone_offset(timezone: str) -> int:
+ """Get timezone offset for a given timezone."""
+ # Simplified timezone offset mapping
+ offset_map = {
+ 'America/New_York': -300, # EST/EDT
+ 'America/Los_Angeles': -480, # PST/PDT
+ 'America/Chicago': -360, # CST/CDT
+ 'Europe/London': 0, # GMT/BST
+ 'Europe/Paris': 60, # CET/CEST
+ 'Europe/Berlin': 60, # CET/CEST
+ 'Asia/Tokyo': 540, # JST
+ 'Asia/Shanghai': 480, # CST
+ 'Asia/Seoul': 540, # KST
+ 'Australia/Sydney': 600, # AEST/AEDT
+ 'America/Toronto': -300, # EST/EDT
+ 'Europe/Madrid': 60, # CET/CEST
+ }
+
+ return offset_map.get(timezone, 0)
+
+ @staticmethod
+ def _generate_unique_properties() -> Dict:
+ """Generate unique properties to ensure fingerprint uniqueness."""
+ # Generate a unique fingerprint ID that won't affect browser functionality
+ unique_id = f"{int(time.time())}_{uuid.uuid4().hex[:8]}"
+
+ # These properties will be added to the fingerprint but won't affect browser functionality
+ # They only ensure that each fingerprint is unique
+ return {
+ "unique_id": unique_id,
+ }
+
+ @staticmethod
+ def _generate_canvas_fingerprint() -> str:
+ """Generate a unique canvas fingerprint."""
+ # Use timestamp and UUID to generate a truly unique canvas fingerprint
+ timestamp = int(time.time() * 1000)
+ random_data = ''.join(random.choices(string.ascii_letters + string.digits, k=24))
+ return f"canvas_fp_{random_data}_{timestamp}"
+
+ def _generate_plugins(self) -> List[Dict[str, str]]:
+ """Generate a realistic list of browser plugins."""
+ common_plugins = [
+ {
+ 'name': 'PDF Viewer',
+ 'filename': 'internal-pdf-viewer',
+ 'description': 'Portable Document Format'
+ },
+ {
+ 'name': 'Chrome PDF Viewer',
+ 'filename': 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
+ 'description': ''
+ },
+ {
+ 'name': 'Chromium PDF Viewer',
+ 'filename': 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
+ 'description': ''
+ },
+ {
+ 'name': 'Microsoft Edge PDF Viewer',
+ 'filename': 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
+ 'description': ''
+ },
+ {
+ 'name': 'WebKit built-in PDF',
+ 'filename': 'webkit-built-in-pdf',
+ 'description': ''
+ },
+ ]
+
+ # Randomly select and return plugins
+ num_plugins = min(random.randint(1, self.config.max_plugins), len(common_plugins))
+ return random.sample(common_plugins, num_plugins)
From c7ff166baedcd73cbf980161352e6fcc9858b2e1 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 15:04:41 +0800
Subject: [PATCH 10/61] feat(fingerprint): add fingerprint module
initialization and exports
Add module initialization file to expose fingerprint spoofing components with clean public API and global manager instance.
---
pydoll/fingerprint/injector.py | 156 +++++++++++++++++++++++++++++++++
1 file changed, 156 insertions(+)
create mode 100644 pydoll/fingerprint/injector.py
diff --git a/pydoll/fingerprint/injector.py b/pydoll/fingerprint/injector.py
new file mode 100644
index 00000000..148d36ce
--- /dev/null
+++ b/pydoll/fingerprint/injector.py
@@ -0,0 +1,156 @@
+"""
+Browser fingerprint injection module.
+
+This module provides the FingerprintInjector class, which generates JavaScript
+code for fingerprint spoofing.
+"""
+
+from ..constants import Scripts
+from .models import Fingerprint
+
+
+class FingerprintInjector:
+ """
+ Generates JavaScript code to inject fingerprint spoofing into browsers.
+
+ This class creates JavaScript that overrides various browser APIs and
+ properties to present a fake fingerprint to detection systems.
+ """
+
+ def __init__(self, fingerprint: Fingerprint):
+ """
+ Initialize the injector with a fingerprint.
+
+ Args:
+ fingerprint: The fingerprint data to inject.
+ """
+ self.fingerprint = fingerprint
+
+ def generate_script(self) -> str:
+ """
+ Generate the complete JavaScript injection script.
+
+ Returns:
+ JavaScript code as a string that will override browser properties.
+ """
+ scripts = [
+ self._generate_navigator_override(),
+ self._generate_screen_override(),
+ self._generate_webgl_override(),
+ self._generate_canvas_override(),
+ self._generate_audio_override(),
+ self._generate_plugin_override(),
+ self._generate_misc_overrides(),
+ ]
+
+ # Use the script template from constants
+ return Scripts.FINGERPRINT_WRAPPER.format(scripts=chr(10).join(scripts))
+
+ def _generate_navigator_override(self) -> str:
+ """Generate JavaScript to override navigator properties."""
+ languages_js = str(self.fingerprint.languages).replace("'", '"')
+
+ # Conditional script parts
+ device_memory_script = (
+ 'Object.defineProperty(navigator, "deviceMemory", { get: () => ' +
+ str(self.fingerprint.device_memory) +
+ ', configurable: true });'
+ ) if self.fingerprint.device_memory else ''
+
+ do_not_track_script = (
+ 'Object.defineProperty(navigator, "doNotTrack", { get: () => "' +
+ str(self.fingerprint.do_not_track) +
+ '", configurable: true });'
+ ) if self.fingerprint.do_not_track else ''
+
+ # Use the script template from constants
+ return Scripts.NAVIGATOR_OVERRIDE.format(
+ user_agent=self.fingerprint.user_agent,
+ platform=self.fingerprint.platform,
+ language=self.fingerprint.language,
+ languages=languages_js,
+ hardware_concurrency=self.fingerprint.hardware_concurrency,
+ device_memory_script=device_memory_script,
+ cookie_enabled=str(self.fingerprint.cookie_enabled).lower(),
+ do_not_track_script=do_not_track_script
+ )
+
+ def _generate_screen_override(self) -> str:
+ """Generate JavaScript to override screen properties."""
+ # Use the script template from constants
+ return Scripts.SCREEN_OVERRIDE.format(
+ screen_width=self.fingerprint.screen_width,
+ screen_height=self.fingerprint.screen_height,
+ screen_color_depth=self.fingerprint.screen_color_depth,
+ screen_pixel_depth=self.fingerprint.screen_pixel_depth,
+ available_width=self.fingerprint.available_width,
+ available_height=self.fingerprint.available_height,
+ inner_width=self.fingerprint.inner_width,
+ inner_height=self.fingerprint.inner_height,
+ viewport_width=self.fingerprint.viewport_width,
+ viewport_height=self.fingerprint.viewport_height
+ )
+
+ def _generate_webgl_override(self) -> str:
+ """Generate JavaScript to override WebGL properties."""
+ extensions_js = str(self.fingerprint.webgl_extensions).replace("'", '"')
+
+ # Use the script template from constants
+ return Scripts.WEBGL_OVERRIDE.format(
+ webgl_vendor=self.fingerprint.webgl_vendor,
+ webgl_renderer=self.fingerprint.webgl_renderer,
+ webgl_version=self.fingerprint.webgl_version,
+ webgl_shading_language_version=self.fingerprint.webgl_shading_language_version,
+ webgl_extensions=extensions_js
+ )
+
+ def _generate_canvas_override(self) -> str:
+ """Generate JavaScript to override canvas fingerprinting."""
+ # Use the script template from constants
+ return Scripts.CANVAS_OVERRIDE.format(
+ canvas_fingerprint=self.fingerprint.canvas_fingerprint
+ )
+
+ def _generate_audio_override(self) -> str:
+ """Generate JavaScript to override audio context properties."""
+ # Use the script template from constants
+ return Scripts.AUDIO_OVERRIDE.format(
+ audio_context_sample_rate=self.fingerprint.audio_context_sample_rate,
+ audio_context_state=self.fingerprint.audio_context_state,
+ audio_context_max_channel_count=self.fingerprint.audio_context_max_channel_count
+ )
+
+ def _generate_plugin_override(self) -> str:
+ """Generate JavaScript to override plugin information."""
+ if not self.fingerprint.plugins:
+ return ""
+
+ plugins_js = []
+ for i, plugin in enumerate(self.fingerprint.plugins):
+ plugins_js.append(f"""
+ plugins[{i}] = {{
+ name: '{plugin['name']}',
+ filename: '{plugin['filename']}',
+ description: '{plugin['description']}',
+ length: 1,
+ 0: {{
+ type: 'application/pdf',
+ suffixes: 'pdf',
+ description: '{plugin['description']}'
+ }}
+ }};""")
+
+ # Use the script template from constants
+ return Scripts.PLUGIN_OVERRIDE.format(
+ plugins_length=len(self.fingerprint.plugins),
+ plugins_js=''.join(plugins_js)
+ )
+
+ def _generate_misc_overrides(self) -> str:
+ """Generate JavaScript for miscellaneous overrides."""
+ # Use the script template from constants
+ return Scripts.MISC_OVERRIDES.format(
+ timezone_offset=self.fingerprint.timezone_offset,
+ timezone=self.fingerprint.timezone,
+ connection_type=self.fingerprint.connection_type
+ )
From 5e30ad9aa7939682bcc49058e9d63d45314fd9f2 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 15:05:00 +0800
Subject: [PATCH 11/61] feat(fingerprint): add fingerprint lifecycle management
system
Add comprehensive fingerprint management with generation, storage,persistence, and lifecycle control for browser automation.
---
pydoll/fingerprint/manager.py | 275 ++++++++++++++++++++++++++++++++++
1 file changed, 275 insertions(+)
create mode 100644 pydoll/fingerprint/manager.py
diff --git a/pydoll/fingerprint/manager.py b/pydoll/fingerprint/manager.py
new file mode 100644
index 00000000..afdf3a95
--- /dev/null
+++ b/pydoll/fingerprint/manager.py
@@ -0,0 +1,275 @@
+"""
+Browser fingerprint management module.
+
+This module provides the FingerprintManager class which coordinates fingerprint
+generation, storage, and application to browser instances.
+"""
+
+import json
+from pathlib import Path
+from typing import Dict, List, Optional
+
+from .generator import FingerprintGenerator
+from .injector import FingerprintInjector
+from .models import Fingerprint, FingerprintConfig
+
+
+class FingerprintManager:
+ """
+ Manages browser fingerprint generation and application.
+
+ This class coordinates the entire fingerprint spoofing process, from
+ generation to injection into browser instances.
+ """
+
+ def __init__(self, config: Optional[FingerprintConfig] = None):
+ """
+ Initialize the fingerprint manager.
+
+ Args:
+ config: Configuration for fingerprint generation. Uses default if None.
+ """
+ self.config = config or FingerprintConfig()
+ self.generator = FingerprintGenerator(self.config)
+ self.current_fingerprint: Optional[Fingerprint] = None
+ self.injector: Optional[FingerprintInjector] = None
+
+ # Storage directory for fingerprints
+ self.storage_dir = Path.home() / '.pydoll' / 'fingerprints'
+ self.storage_dir.mkdir(parents=True, exist_ok=True)
+
+ def generate_new_fingerprint(
+ self, browser_type: str = 'chrome', force: bool = False
+ ) -> Fingerprint:
+ """
+ Generate a new browser fingerprint.
+
+ Args:
+ browser_type: Type of browser ('chrome' or 'edge').
+ force: Whether to force generation of a new fingerprint even if one exists.
+
+ Returns:
+ The generated fingerprint.
+ """
+ if self.current_fingerprint and not force:
+ return self.current_fingerprint
+
+ # Update config with browser type
+ self.config.browser_type = browser_type
+ self.generator.config.browser_type = browser_type
+
+ # Generate new fingerprint
+ self.current_fingerprint = self.generator.generate()
+
+ # Create injector for this fingerprint
+ self.injector = FingerprintInjector(self.current_fingerprint)
+
+ return self.current_fingerprint
+
+ def get_current_fingerprint(self) -> Optional[Fingerprint]:
+ """
+ Get the current active fingerprint.
+
+ Returns:
+ The current fingerprint or None if none has been generated.
+ """
+ return self.current_fingerprint
+
+ def get_fingerprint_js(self) -> str:
+ """
+ Get JavaScript injection code for the current fingerprint.
+
+ Returns:
+ JavaScript code as a string.
+
+ Raises:
+ ValueError: If no fingerprint has been generated.
+ """
+ if not self.injector:
+ raise ValueError(
+ "No fingerprint has been generated. Call generate_new_fingerprint() first."
+ )
+
+ return self.injector.generate_script()
+
+ def get_fingerprint_arguments(self, browser_type: str = 'chrome') -> List[str]:
+ """
+ Get command line arguments for fingerprint spoofing.
+
+ Args:
+ browser_type: Type of browser ('chrome' or 'edge').
+
+ Returns:
+ List of command line arguments.
+
+ Raises:
+ ValueError: If no fingerprint has been generated.
+ """
+ if not self.current_fingerprint:
+ raise ValueError(
+ "No fingerprint has been generated. Call generate_new_fingerprint() first."
+ )
+
+ args = []
+
+ # User agent
+ args.append(f'--user-agent={self.current_fingerprint.user_agent}')
+
+ # Language settings
+ args.append(f'--lang={self.current_fingerprint.language}')
+
+ # Window size
+ args.append(
+ f'--window-size={self.current_fingerprint.viewport_width},'
+ f'{self.current_fingerprint.viewport_height}'
+ )
+
+ # Hardware concurrency (for Chrome)
+ if browser_type == 'chrome':
+ args.append(
+ f'--force-cpu-count={self.current_fingerprint.hardware_concurrency}'
+ )
+
+ # Disable automation features
+ args.extend([
+ '--disable-blink-features=AutomationControlled',
+ '--exclude-switches=enable-automation',
+ '--disable-extensions-except',
+ '--disable-plugins-discovery',
+ '--no-first-run',
+ '--no-service-autorun',
+ '--password-store=basic',
+ '--use-mock-keychain',
+ ])
+
+ return args
+
+ def save_fingerprint(self, name: str, fingerprint: Optional[Fingerprint] = None) -> str:
+ """
+ Save a fingerprint to disk for later reuse.
+
+ Args:
+ name: Name to save the fingerprint under.
+ fingerprint: Fingerprint to save. Uses current if None.
+
+ Returns:
+ Path where the fingerprint was saved.
+
+ Raises:
+ ValueError: If no fingerprint is provided and none is current.
+ """
+ fp = fingerprint or self.current_fingerprint
+ if not fp:
+ raise ValueError("No fingerprint provided and no current fingerprint exists.")
+
+ # Create filename
+ filename = f"{name}.json"
+ filepath = self.storage_dir / filename
+
+ # Save fingerprint
+ with open(filepath, 'w', encoding='utf-8') as f:
+ json.dump(fp.to_dict(), f, indent=2, ensure_ascii=False)
+
+ return str(filepath)
+
+ def load_fingerprint(self, name: str) -> Fingerprint:
+ """
+ Load a saved fingerprint from disk.
+
+ Args:
+ name: Name of the fingerprint to load.
+
+ Returns:
+ The loaded fingerprint.
+
+ Raises:
+ FileNotFoundError: If the fingerprint file doesn't exist.
+ ValueError: If the fingerprint file is invalid.
+ """
+ filename = f"{name}.json"
+ filepath = self.storage_dir / filename
+
+ if not filepath.exists():
+ raise FileNotFoundError(f"Fingerprint '{name}' not found at {filepath}")
+
+ try:
+ with open(filepath, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+
+ fingerprint = Fingerprint.from_dict(data)
+
+ # Set as current fingerprint and create injector
+ self.current_fingerprint = fingerprint
+ self.injector = FingerprintInjector(fingerprint)
+
+ return fingerprint
+
+ except (json.JSONDecodeError, TypeError, KeyError) as e:
+ raise ValueError(f"Invalid fingerprint file '{name}': {e}")
+
+ def list_saved_fingerprints(self) -> List[str]:
+ """
+ List all saved fingerprints.
+
+ Returns:
+ List of fingerprint names.
+ """
+ fingerprints = []
+ for filepath in self.storage_dir.glob("*.json"):
+ fingerprints.append(filepath.stem)
+ return sorted(fingerprints)
+
+ def delete_fingerprint(self, name: str) -> bool:
+ """
+ Delete a saved fingerprint.
+
+ Args:
+ name: Name of the fingerprint to delete.
+
+ Returns:
+ True if deleted successfully, False if file didn't exist.
+ """
+ filename = f"{name}.json"
+ filepath = self.storage_dir / filename
+
+ if filepath.exists():
+ filepath.unlink()
+ return True
+ return False
+
+ def clear_current_fingerprint(self):
+ """Clear the current fingerprint and injector."""
+ self.current_fingerprint = None
+ self.injector = None
+
+ def get_fingerprint_summary(self, fingerprint: Optional[Fingerprint] = None) -> Dict[str, str]:
+ """
+ Get a human-readable summary of a fingerprint.
+
+ Args:
+ fingerprint: Fingerprint to summarize. Uses current if None.
+
+ Returns:
+ Dictionary with fingerprint summary information.
+
+ Raises:
+ ValueError: If no fingerprint is provided and none is current.
+ """
+ fp = fingerprint or self.current_fingerprint
+ if not fp:
+ raise ValueError("No fingerprint provided and no current fingerprint exists.")
+
+ return {
+ 'Browser': f"{fp.browser_type.title()} {fp.browser_version}",
+ 'User Agent': fp.user_agent,
+ 'Platform': fp.platform,
+ 'Language': fp.language,
+ 'Screen': f"{fp.screen_width}x{fp.screen_height}",
+ 'Viewport': f"{fp.viewport_width}x{fp.viewport_height}",
+ 'WebGL Vendor': fp.webgl_vendor,
+ 'WebGL Renderer': fp.webgl_renderer,
+ 'Hardware Concurrency': str(fp.hardware_concurrency),
+ 'Device Memory': f"{fp.device_memory}GB" if fp.device_memory else "Not set",
+ 'Timezone': fp.timezone,
+ 'Canvas Fingerprint': fp.canvas_fingerprint[:32] + "...",
+ }
From b1996b447b839ef9d78b2796370aec69d3d8af6f Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 15:05:21 +0800
Subject: [PATCH 12/61] feat(fingerprint): add fingerprint data models and
configuration classes
Add comprehensive data structures for fingerprint spoofing configuration and generated fingerprint storage with serialization support.
---
pydoll/fingerprint/models.py | 139 +++++++++++++++++++++++++++++++++++
1 file changed, 139 insertions(+)
create mode 100644 pydoll/fingerprint/models.py
diff --git a/pydoll/fingerprint/models.py b/pydoll/fingerprint/models.py
new file mode 100644
index 00000000..fba914c3
--- /dev/null
+++ b/pydoll/fingerprint/models.py
@@ -0,0 +1,139 @@
+"""
+Data models for browser fingerprint spoofing.
+
+This module defines the data structures used to represent browser fingerprints
+and their configuration parameters.
+"""
+
+from dataclasses import dataclass
+from typing import Dict, List, Optional, Union
+
+
+@dataclass
+class Fingerprint:
+ """
+ Represents a complete browser fingerprint configuration.
+
+ This class contains all the necessary data to spoof a browser's fingerprint,
+ including navigator properties, screen dimensions, WebGL settings, and more.
+ """
+
+ # Navigator properties (required fields first)
+ user_agent: str
+ platform: str
+ language: str
+ languages: List[str]
+ hardware_concurrency: int
+
+ # Screen properties
+ screen_width: int
+ screen_height: int
+ screen_color_depth: int
+ screen_pixel_depth: int
+ available_width: int
+ available_height: int
+
+ # Viewport properties
+ viewport_width: int
+ viewport_height: int
+ inner_width: int
+ inner_height: int
+
+ # WebGL properties
+ webgl_vendor: str
+ webgl_renderer: str
+ webgl_version: str
+ webgl_shading_language_version: str
+ webgl_extensions: List[str]
+
+ # Canvas fingerprint
+ canvas_fingerprint: str
+
+ # Audio context properties
+ audio_context_sample_rate: float
+ audio_context_state: str
+ audio_context_max_channel_count: int
+
+ # Timezone and locale
+ timezone: str
+ timezone_offset: int
+
+ # Browser specific
+ browser_type: str
+ browser_version: str
+
+ # Plugin information
+ plugins: List[Dict[str, str]]
+
+ # Optional properties with defaults (must come last)
+ device_memory: Optional[int] = None
+ chrome_version: Optional[str] = None
+ cookie_enabled: bool = True
+ do_not_track: Optional[str] = None
+ webdriver: bool = False
+ connection_type: str = "wifi"
+ unique_id: Optional[str] = None
+
+ def to_dict(self) -> Dict[str, Union[str, int, float, bool, List, None]]:
+ """Convert fingerprint to dictionary format."""
+ result = {}
+ for key, value in self.__dict__.items():
+ result[key] = value
+ return result
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Union[str, int, float, bool, List, None]]) -> 'Fingerprint':
+ """Create fingerprint from dictionary format."""
+ return cls(**data) # type: ignore[arg-type]
+
+
+@dataclass
+class FingerprintConfig:
+ """
+ Configuration for fingerprint generation.
+
+ This class defines the parameters that control how fingerprints are generated,
+ allowing for customization of the spoofing behavior.
+ """
+
+ # Browser configuration
+ browser_type: str = "chrome"
+ is_mobile: bool = False
+
+ # Operating system preferences
+ preferred_os: Optional[str] = None # "windows", "macos", "linux"
+
+ # Language and locale preferences
+ preferred_languages: Optional[List[str]] = None
+ preferred_timezone: Optional[str] = None
+
+ # Screen configuration
+ min_screen_width: int = 1024
+ max_screen_width: int = 2560
+ min_screen_height: int = 768
+ max_screen_height: int = 1440
+
+ # WebGL configuration
+ enable_webgl_spoofing: bool = True
+ enable_canvas_spoofing: bool = True
+ enable_audio_spoofing: bool = True
+
+ # Plugin configuration
+ include_plugins: bool = True
+ max_plugins: int = 5
+
+ # Advanced options
+ enable_touch_support: bool = False
+ enable_webrtc_spoofing: bool = True
+
+ def to_dict(self) -> Dict[str, Union[str, int, bool, List, None]]:
+ """Convert config to dictionary format."""
+ result = {}
+ for key, value in self.__dict__.items():
+ result[key] = value
+ return result
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Union[str, int, bool, List, None]]) -> 'FingerprintConfig':
+ """Create config from dictionary format."""
+ return cls(**data) # type: ignore[arg-type]
From 39d97c9929a37de467972dbf8454a1c9b5c17264 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 15:09:02 +0800
Subject: [PATCH 13/61] feat(browser): add fingerprint spoofing integration to
browser base class
Add comprehensive fingerprint spoofing support to the core Browser class with script injection and fingerprint management capabilities.
- Add fingerprint_manager and enable_fingerprint_spoofing attributes
- Add _inject_fingerprint_script() method for JavaScript injection
- Add get_fingerprint_summary() method for fingerprint information
- Add fingerprint script injection to start() method workflow
- Add fingerprint manager reference storage from options manager
- Add support for Chrome and Edge fingerprint spoofing
- Add comprehensive fingerprint JavaScript injection with stealth features
---
.../managers/browser_options_manager.py | 63 ++++++++++++++++++-
1 file changed, 61 insertions(+), 2 deletions(-)
diff --git a/pydoll/browser/managers/browser_options_manager.py b/pydoll/browser/managers/browser_options_manager.py
index 54fda7b5..fe5f5384 100644
--- a/pydoll/browser/managers/browser_options_manager.py
+++ b/pydoll/browser/managers/browser_options_manager.py
@@ -3,6 +3,7 @@
from pydoll.browser.interfaces import BrowserOptionsManager, Options
from pydoll.browser.options import ChromiumOptions
from pydoll.exceptions import InvalidOptionsObject
+from pydoll.fingerprint.manager import FingerprintManager
class ChromiumOptionsManager(BrowserOptionsManager):
@@ -13,8 +14,20 @@ class ChromiumOptionsManager(BrowserOptionsManager):
for Chrome and Edge browsers.
"""
- def __init__(self, options: Optional[Options] = None):
+ def __init__(
+ self,
+ options: Optional[Options] = None,
+ enable_fingerprint_spoofing: bool = False,
+ fingerprint_config=None
+ ):
self.options = options
+ self.enable_fingerprint_spoofing = enable_fingerprint_spoofing
+ self.fingerprint_config = fingerprint_config
+ self.fingerprint_manager = None
+
+ # Initialize fingerprint manager if spoofing is enabled
+ if enable_fingerprint_spoofing:
+ self.fingerprint_manager = FingerprintManager(fingerprint_config)
def initialize_options(
self,
@@ -23,7 +36,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 +51,55 @@ def initialize_options(
raise InvalidOptionsObject(f'Expected ChromiumOptions, got {type(self.options)}')
self.add_default_arguments()
+
+ # Apply fingerprint spoofing if enabled
+ if self.enable_fingerprint_spoofing and self.fingerprint_manager:
+ 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')
+
+ 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
+ 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):
+ """
+ Get the fingerprint manager instance.
+
+ Returns:
+ The fingerprint manager if fingerprint spoofing is enabled, None otherwise.
+ """
+ return self.fingerprint_manager
From 3a2bc4ddb245110ad7cf121d5d99aeeeabc00d3e Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 15:10:21 +0800
Subject: [PATCH 14/61] feat(browser): add fingerprint spoofing integration to
browser base class
Add comprehensive fingerprint spoofing support to the core Browser class with script injection and fingerprint management capabilities.
- Add fingerprint_manager and enable_fingerprint_spoofing attributes
- Add _inject_fingerprint_script() method for JavaScript injection
- Add get_fingerprint_summary() method for fingerprint information
- Add fingerprint script injection to start() method workflow
- Add fingerprint manager reference storage from options manager
- Add support for Chrome and Edge fingerprint spoofing
- Add comprehensive fingerprint JavaScript injection with stealth features
---
pydoll/browser/chromium/base.py | 73 +++++++++++++++++++++++++++++----
1 file changed, 65 insertions(+), 8 deletions(-)
diff --git a/pydoll/browser/chromium/base.py b/pydoll/browser/chromium/base.py
index 16656882..0724f836 100644
--- a/pydoll/browser/chromium/base.py
+++ b/pydoll/browser/chromium/base.py
@@ -4,16 +4,15 @@
from random import randint
from typing import Any, Callable, Optional, TypeVar
-from pydoll.browser.interfaces import BrowserOptionsManager
-from pydoll.browser.managers import (
- BrowserProcessManager,
- ProxyManager,
- TempDirectoryManager,
-)
+from pydoll.browser.managers.browser_options_manager import BrowserOptionsManager
+from pydoll.browser.managers.browser_process_manager import BrowserProcessManager
+from pydoll.browser.managers.proxy_manager import ProxyManager
+from pydoll.browser.managers.temp_dir_manager import TempDirectoryManager
from pydoll.browser.tab import Tab
from pydoll.commands import (
BrowserCommands,
FetchCommands,
+ PageCommands,
RuntimeCommands,
StorageCommands,
TargetCommands,
@@ -82,6 +81,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
@@ -125,7 +130,13 @@ async def start(self, headless: bool = False) -> Tab:
await self._configure_proxy(proxy_config[0], proxy_config[1])
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
+ if self.enable_fingerprint_spoofing and self.fingerprint_manager:
+ await self._inject_fingerprint_script(tab)
+
+ return tab
async def stop(self):
"""
@@ -208,7 +219,13 @@ async def new_tab(self, url: str = '', browser_context_id: Optional[str] = None)
)
)
target_id = response['result']['targetId']
- return Tab(self, self._connection_port, target_id, browser_context_id)
+ tab = Tab(self, self._connection_port, target_id, browser_context_id)
+
+ # Inject fingerprint spoofing JavaScript if enabled
+ if self.enable_fingerprint_spoofing and self.fingerprint_manager:
+ await self._inject_fingerprint_script(tab)
+
+ return tab
async def get_targets(self) -> list[TargetInfo]:
"""
@@ -584,6 +601,46 @@ 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 _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 Exception:
+ # Ignore errors for immediate execution as page might not be ready
+ pass
+
+ except Exception as e:
+ # Don't let fingerprint injection failures break the browser
+ print(f"Warning: Failed to inject fingerprint spoofing script: {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.
+ """
+ if self.fingerprint_manager:
+ return self.fingerprint_manager.get_fingerprint_summary()
+ return None
+
@abstractmethod
def _get_default_binary_location(self) -> str:
"""Get default browser executable path (implemented by subclasses)."""
From 6529c60f69c002b215d3fac5710f86af318a5798 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 15:10:47 +0800
Subject: [PATCH 15/61] feat(browser): add fingerprint spoofing parameters to
Chrome browser
Add fingerprint spoofing configuration support to Chrome browser class for enhanced automation stealth capabilities.
- Add enable_fingerprint_spoofing parameter to __init__ method
- Add fingerprint_config parameter for custom fingerprint settings
- Add fingerprint_manager attribute for fingerprint management
- Add fingerprint spoofing integration with ChromiumOptionsManager
- Add support for Chrome-specific fingerprint generation and injection
---
pydoll/browser/chromium/chrome.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/pydoll/browser/chromium/chrome.py b/pydoll/browser/chromium/chrome.py
index b73046ec..3af7c07d 100644
--- a/pydoll/browser/chromium/chrome.py
+++ b/pydoll/browser/chromium/chrome.py
@@ -15,6 +15,8 @@ def __init__(
self,
options: Optional[ChromiumOptions] = None,
connection_port: Optional[int] = None,
+ enable_fingerprint_spoofing: bool = False,
+ fingerprint_config=None,
):
"""
Initialize Chrome browser instance.
@@ -22,9 +24,17 @@ def __init__(
Args:
options: Chrome configuration options (default if None).
connection_port: CDP WebSocket port (random if None).
+ enable_fingerprint_spoofing: Whether to enable fingerprint spoofing.
+ fingerprint_config: Configuration for fingerprint generation.
"""
- options_manager = ChromiumOptionsManager(options)
+ options_manager = ChromiumOptionsManager(
+ options,
+ enable_fingerprint_spoofing,
+ fingerprint_config
+ )
super().__init__(options_manager, connection_port)
+ self.enable_fingerprint_spoofing = enable_fingerprint_spoofing
+ self.fingerprint_manager = options_manager.get_fingerprint_manager()
@staticmethod
def _get_default_binary_location():
From 55e99d7801bf0f1092838005eb2aa564cef138d9 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 15:11:09 +0800
Subject: [PATCH 16/61] feat(browser): add fingerprint spoofing parameters to
Edge browser
Add fingerprint spoofing configuration support to Edge browser class for enhanced automation stealth capabilities.
- Add enable_fingerprint_spoofing parameter to __init__ method
- Add fingerprint_config parameter for custom fingerprint settings
- Add fingerprint_manager attribute for fingerprint management
- Add fingerprint spoofing integration with ChromiumOptionsManager
- Add support for Edge-specific fingerprint generation and injection
---
pydoll/browser/chromium/edge.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/pydoll/browser/chromium/edge.py b/pydoll/browser/chromium/edge.py
index e020b04b..c8a922b4 100644
--- a/pydoll/browser/chromium/edge.py
+++ b/pydoll/browser/chromium/edge.py
@@ -15,6 +15,8 @@ def __init__(
self,
options: Optional[Options] = None,
connection_port: Optional[int] = None,
+ enable_fingerprint_spoofing: bool = False,
+ fingerprint_config=None,
):
"""
Initialize Edge browser instance.
@@ -22,9 +24,17 @@ def __init__(
Args:
options: Edge configuration options (default if None).
connection_port: CDP WebSocket port (random if None).
+ enable_fingerprint_spoofing: Whether to enable fingerprint spoofing.
+ fingerprint_config: Configuration for fingerprint generation.
"""
- options_manager = ChromiumOptionsManager(options)
+ options_manager = ChromiumOptionsManager(
+ options,
+ enable_fingerprint_spoofing,
+ fingerprint_config
+ )
super().__init__(options_manager, connection_port)
+ self.enable_fingerprint_spoofing = enable_fingerprint_spoofing
+ self.fingerprint_manager = options_manager.get_fingerprint_manager()
@staticmethod
def _get_default_binary_location():
From 3d8c052f1f757f4a0c242d0e7d4380176940b8d7 Mon Sep 17 00:00:00 2001
From: Tokisaki Kurumi <3201380070@qq.com>
Date: Sat, 12 Jul 2025 15:15:42 +0800
Subject: [PATCH 17/61] refactor(elements): improve option element clicking
implementation
Refactor option element clicking to use more consistent runtime command approach and improve code structure for better maintainability.
---
pydoll/elements/web_element.py | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/pydoll/elements/web_element.py b/pydoll/elements/web_element.py
index 98cc9d97..561e2814 100644
--- a/pydoll/elements/web_element.py
+++ b/pydoll/elements/web_element.py
@@ -345,11 +345,8 @@ async def press_keyboard_key(
async def _click_option_tag(self):
"""Specialized method for clicking