From 2832673f835ef72218c3d25bcf97d80e31b51f84 Mon Sep 17 00:00:00 2001 From: daijro Date: Fri, 4 Oct 2024 18:59:04 -0500 Subject: [PATCH] pythonlib: Add persistent_context, humanize, etc. 0.2.5 - Added `persistent_context` parameter #21 - Added `humanize` parameter #22 - Added validation for custom fingerprints - Updated Browserforge integration documentation --- pythonlib/README.md | 57 ++++++++++++++++++++++----- pythonlib/camoufox/async_api.py | 53 +++++++++++++++++-------- pythonlib/camoufox/exceptions.py | 8 ++++ pythonlib/camoufox/sync_api.py | 51 ++++++++++++++++-------- pythonlib/camoufox/utils.py | 66 +++++++++++++++++++++++++++++--- pythonlib/pyproject.toml | 2 +- 6 files changed, 188 insertions(+), 49 deletions(-) diff --git a/pythonlib/README.md b/pythonlib/README.md index e62e9f3..b324c01 100644 --- a/pythonlib/README.md +++ b/pythonlib/README.md @@ -92,7 +92,7 @@ Parameters: Camoufox properties to use. os (Optional[ListOrString]): Operating system to use for the fingerprint generation. - Can be "windows", "macos", or "linux", or a list of these to choose from randomly. + Can be "windows", "macos", "linux", "android", "ios", or a list to randomly choose from. Default: ["windows", "macos", "linux"] block_images (Optional[bool]): Whether to block all images. @@ -103,6 +103,10 @@ Parameters: geoip (Optional[Union[str, bool]]): Calculate longitude, latitude, timezone, country, & locale based on the IP address. Pass the target IP address to use, or `True` to find the IP address automatically. + humanize (Optional[Union[bool, float]]): + Humanize the cursor movement. + Takes either `True`, or the MAX duration in seconds of the cursor movement. + The cursor typically takes up to 1.5 seconds to move across the window. locale (Optional[str]): Locale to use in Camoufox. addons (Optional[List[str]]): @@ -112,12 +116,16 @@ Parameters: Takes a list of font family names that are installed on the system. exclude_addons (Optional[List[DefaultAddons]]): Default addons to exclude. Passed as a list of camoufox.DefaultAddons enums. - fingerprint (Optional[Fingerprint]): - Use a custom BrowserForge fingerprint. Note: Not all values will be implemented. - If not provided, a random fingerprint will be generated based on the provided os & user_agent. screen (Optional[Screen]): Constrains the screen dimensions of the generated fingerprint. Takes a browserforge.fingerprints.Screen instance. + fingerprint (Optional[Fingerprint]): + Use a custom BrowserForge fingerprint. Note: Not all values will be implemented. + If not provided, a random fingerprint will be generated based on the provided + `os` & `screen` constraints. + ff_version (Optional[int]): + Firefox version to use. Defaults to the current Camoufox version. + To prevent leaks, only use this for special cases. headless (Optional[bool]): Whether to run the browser in headless mode. Defaults to True. executable_path (Optional[str]): @@ -127,13 +135,14 @@ Parameters: proxy (Optional[Dict[str, str]]): Proxy to use for the browser. Note: If geoip is True, a request will be sent through this proxy to find the target IP. - ff_version (Optional[int]): - Firefox version to use. Defaults to the current Camoufox version. - To prevent leaks, only use this for special cases. args (Optional[List[str]]): Arguments to pass to the browser. env (Optional[Dict[str, Union[str, float, bool]]]): Environment variables to set. + persistent_context (Optional[bool]): + Whether to use a persistent context. + debug (Optional[bool]): + Prints the config being sent to Camoufox. **launch_options (Dict[str, Any]): Additional Firefox launch options. ``` @@ -245,7 +254,35 @@ with sync_playwright() as p: Camoufox is compatible with [BrowserForge](https://github.com/daijro/browserforge) fingerprints. -By default, Camoufox will use a random fingerprint. You can also inject your own Firefox Browserforge fingerprint into Camoufox with the following example: +By default, Camoufox will generate an use a random BrowserForge fingerprint based on the target `os` & `screen` constraints. + +```python +from camoufox.sync_api import Camoufox +from browserforge.fingerprints import Screen + +with Camoufox( + os=('windows', 'macos', 'linux'), + screen=Screen(max_width=1920, max_height=1080), +) as browser: + page = browser.new_page() + page.goto("https://example.com/") +``` + +**Notes:** + +- If Camoufox is being ran in headful mode, the max screen size will be generated based on your monitor's dimensions (+15%). + +- To prevent UA-spoofing leaks, Camoufox only generates fingerprints with the same browser version as the current Camoufox version by default. + + - If rotating the Firefox version is absolutely necessary, it would be more advisable to rotate between older versions of Camoufox instead. + +
+Injecting custom Fingerprint objects... + +> [!WARNING] +> It is recommended to pass `os` & `screen` constraints into Camoufox instead. Camoufox will handle fingerprint generation for you. This will be deprecated in the future. + +You can also inject your own Firefox BrowserForge fingerprint into Camoufox. ```python from camoufox.sync_api import Camoufox @@ -259,8 +296,8 @@ with Camoufox(fingerprint=fg.generate()) as browser: page.goto("https://example.com/") ``` -
- **Note:** As of now, some properties from BrowserForge fingerprints will not be passed to Camoufox. This is due to the outdated fingerprint dataset from Apify's fingerprint-suite (see [here](https://github.com/apify/fingerprint-suite/discussions/308)). Properties will be re-enabled as soon as an updated dataset is available. +
+ --- diff --git a/pythonlib/camoufox/async_api.py b/pythonlib/camoufox/async_api.py index 4bc495d..843aba0 100644 --- a/pythonlib/camoufox/async_api.py +++ b/pythonlib/camoufox/async_api.py @@ -1,10 +1,15 @@ from typing import Any, Dict, List, Optional, Union from browserforge.fingerprints import Fingerprint, Screen -from playwright.async_api import Browser, Playwright, PlaywrightContextManager +from playwright.async_api import ( + Browser, + BrowserContext, + Playwright, + PlaywrightContextManager, +) from .addons import DefaultAddons -from .utils import ListOrString, get_launch_options +from .utils import ListOrString, _clean_locals, get_launch_options class AsyncCamoufox(PlaywrightContextManager): @@ -16,9 +21,9 @@ class AsyncCamoufox(PlaywrightContextManager): def __init__(self, **launch_options): super().__init__() self.launch_options = launch_options - self.browser: Optional[Browser] = None + self.browser: Optional[Union[Browser, BrowserContext]] = None - async def __aenter__(self) -> Browser: + async def __aenter__(self) -> Union[Browser, BrowserContext]: _playwright = await super().__aenter__() self.browser = await AsyncNewBrowser(_playwright, **self.launch_options) return self.browser @@ -38,21 +43,25 @@ async def AsyncNewBrowser( block_webrtc: Optional[bool] = None, allow_webgl: Optional[bool] = None, geoip: Optional[Union[str, bool]] = None, + humanize: Optional[Union[bool, float]] = None, locale: Optional[str] = None, addons: Optional[List[str]] = None, fonts: Optional[List[str]] = None, exclude_addons: Optional[List[DefaultAddons]] = None, - fingerprint: Optional[Fingerprint] = None, screen: Optional[Screen] = None, + fingerprint: Optional[Fingerprint] = None, + ff_version: Optional[int] = None, headless: Optional[bool] = None, executable_path: Optional[str] = None, firefox_user_prefs: Optional[Dict[str, Any]] = None, proxy: Optional[Dict[str, str]] = None, - ff_version: Optional[int] = None, args: Optional[List[str]] = None, env: Optional[Dict[str, Union[str, float, bool]]] = None, + persistent_context: Optional[bool] = None, + i_know_what_im_doing: Optional[bool] = None, + debug: Optional[bool] = None, **launch_options: Dict[str, Any] -) -> Browser: +) -> Union[Browser, BrowserContext]: """ Launches a new browser instance for Camoufox. Accepts all Playwright Firefox launch options, along with the following: @@ -62,7 +71,7 @@ async def AsyncNewBrowser( Camoufox properties to use. (read https://github.com/daijro/camoufox/blob/main/README.md) os (Optional[ListOrString]): Operating system to use for the fingerprint generation. - Can be "windows", "macos", or "linux", or a list of these to choose from randomly. + Can be "windows", "macos", "linux", "android", "ios", or a list to randomly choose from. Default: ["windows", "macos", "linux"] block_images (Optional[bool]): Whether to block all images. @@ -73,6 +82,10 @@ async def AsyncNewBrowser( geoip (Optional[Union[str, bool]]): Calculate longitude, latitude, timezone, country, & locale based on the IP address. Pass the target IP address to use, or `True` to find the IP address automatically. + humanize (Optional[Union[bool, float]]): + Humanize the cursor movement. + Takes either `True`, or the MAX duration in seconds of the cursor movement. + The cursor typically takes up to 1.5 seconds to move across the window. locale (Optional[str]): Locale to use in Camoufox. addons (Optional[List[str]]): @@ -82,12 +95,16 @@ async def AsyncNewBrowser( Takes a list of font family names that are installed on the system. exclude_addons (Optional[List[DefaultAddons]]): Default addons to exclude. Passed as a list of camoufox.DefaultAddons enums. - fingerprint (Optional[Fingerprint]): - Use a custom BrowserForge fingerprint. Note: Not all values will be implemented. - If not provided, a random fingerprint will be generated based on the provided os & user_agent. screen (Optional[Screen]): Constrains the screen dimensions of the generated fingerprint. Takes a browserforge.fingerprints.Screen instance. + fingerprint (Optional[Fingerprint]): + Use a custom BrowserForge fingerprint. Note: Not all values will be implemented. + If not provided, a random fingerprint will be generated based on the provided + `os` & `screen` constraints. + ff_version (Optional[int]): + Firefox version to use. Defaults to the current Camoufox version. + To prevent leaks, only use this for special cases. headless (Optional[bool]): Whether to run the browser in headless mode. Defaults to True. executable_path (Optional[str]): @@ -97,18 +114,20 @@ async def AsyncNewBrowser( proxy (Optional[Dict[str, str]]): Proxy to use for the browser. Note: If geoip is True, a request will be sent through this proxy to find the target IP. - ff_version (Optional[int]): - Firefox version to use. Defaults to the current Camoufox version. - To prevent leaks, only use this for special cases. args (Optional[List[str]]): Arguments to pass to the browser. env (Optional[Dict[str, Union[str, float, bool]]]): Environment variables to set. + persistent_context (Optional[bool]): + Whether to use a persistent context. + debug (Optional[bool]): + Prints the config being sent to Camoufox. **launch_options (Dict[str, Any]): Additional Firefox launch options. """ - data = locals() - data.pop('playwright') + opt = get_launch_options(**_clean_locals(locals())) + + if persistent_context: + return await playwright.firefox.launch_persistent_context(**opt) - opt = get_launch_options(**data) return await playwright.firefox.launch(**opt) diff --git a/pythonlib/camoufox/exceptions.py b/pythonlib/camoufox/exceptions.py index e646475..fcad52a 100644 --- a/pythonlib/camoufox/exceptions.py +++ b/pythonlib/camoufox/exceptions.py @@ -100,3 +100,11 @@ class NotInstalledGeoIPExtra(ImportError): """ ... + + +class NonFirefoxFingerprint(Exception): + """ + Raised when a passed Browserforge fingerprint is invalid. + """ + + ... diff --git a/pythonlib/camoufox/sync_api.py b/pythonlib/camoufox/sync_api.py index 7f45651..e3a5098 100644 --- a/pythonlib/camoufox/sync_api.py +++ b/pythonlib/camoufox/sync_api.py @@ -1,10 +1,15 @@ from typing import Any, Dict, List, Optional, Union from browserforge.fingerprints import Fingerprint, Screen -from playwright.sync_api import Browser, Playwright, PlaywrightContextManager +from playwright.sync_api import ( + Browser, + BrowserContext, + Playwright, + PlaywrightContextManager, +) from .addons import DefaultAddons -from .utils import ListOrString, get_launch_options +from .utils import ListOrString, _clean_locals, get_launch_options class Camoufox(PlaywrightContextManager): @@ -16,9 +21,9 @@ class Camoufox(PlaywrightContextManager): def __init__(self, **launch_options): super().__init__() self.launch_options = launch_options - self.browser: Optional[Browser] = None + self.browser: Optional[Union[Browser, BrowserContext]] = None - def __enter__(self) -> Browser: + def __enter__(self) -> Union[Browser, BrowserContext]: super().__enter__() self.browser = NewBrowser(self._playwright, **self.launch_options) return self.browser @@ -38,19 +43,23 @@ def NewBrowser( block_webrtc: Optional[bool] = None, allow_webgl: Optional[bool] = None, geoip: Optional[Union[str, bool]] = None, + humanize: Optional[Union[bool, float]] = None, locale: Optional[str] = None, addons: Optional[List[str]] = None, fonts: Optional[List[str]] = None, exclude_addons: Optional[List[DefaultAddons]] = None, - fingerprint: Optional[Fingerprint] = None, screen: Optional[Screen] = None, + fingerprint: Optional[Fingerprint] = None, + ff_version: Optional[int] = None, headless: Optional[bool] = None, executable_path: Optional[str] = None, firefox_user_prefs: Optional[Dict[str, Any]] = None, proxy: Optional[Dict[str, str]] = None, - ff_version: Optional[int] = None, args: Optional[List[str]] = None, env: Optional[Dict[str, Union[str, float, bool]]] = None, + persistent_context: Optional[bool] = None, + i_know_what_im_doing: Optional[bool] = None, + debug: Optional[bool] = None, **launch_options: Dict[str, Any] ) -> Browser: """ @@ -62,7 +71,7 @@ def NewBrowser( Camoufox properties to use. (read https://github.com/daijro/camoufox/blob/main/README.md) os (Optional[ListOrString]): Operating system to use for the fingerprint generation. - Can be "windows", "macos", or "linux", or a list of these to choose from randomly. + Can be "windows", "macos", "linux", "android", "ios", or a list to randomly choose from. Default: ["windows", "macos", "linux"] block_images (Optional[bool]): Whether to block all images. @@ -73,6 +82,10 @@ def NewBrowser( geoip (Optional[Union[str, bool]]): Calculate longitude, latitude, timezone, country, & locale based on the IP address. Pass the target IP address to use, or `True` to find the IP address automatically. + humanize (Optional[Union[bool, float]]): + Humanize the cursor movement. + Takes either `True`, or the MAX duration in seconds of the cursor movement. + The cursor typically takes up to 1.5 seconds to move across the window. locale (Optional[str]): Locale to use in Camoufox. addons (Optional[List[str]]): @@ -82,12 +95,16 @@ def NewBrowser( Takes a list of font family names that are installed on the system. exclude_addons (Optional[List[DefaultAddons]]): Default addons to exclude. Passed as a list of camoufox.DefaultAddons enums. - fingerprint (Optional[Fingerprint]): - Use a custom BrowserForge fingerprint. Note: Not all values will be implemented. - If not provided, a random fingerprint will be generated based on the provided os & user_agent. screen (Optional[Screen]): Constrains the screen dimensions of the generated fingerprint. Takes a browserforge.fingerprints.Screen instance. + fingerprint (Optional[Fingerprint]): + Use a custom BrowserForge fingerprint. Note: Not all values will be implemented. + If not provided, a random fingerprint will be generated based on the provided + `os` & `screen` constraints. + ff_version (Optional[int]): + Firefox version to use. Defaults to the current Camoufox version. + To prevent leaks, only use this for special cases. headless (Optional[bool]): Whether to run the browser in headless mode. Defaults to True. executable_path (Optional[str]): @@ -97,18 +114,20 @@ def NewBrowser( proxy (Optional[Dict[str, str]]): Proxy to use for the browser. Note: If geoip is True, a request will be sent through this proxy to find the target IP. - ff_version (Optional[int]): - Firefox version to use. Defaults to the current Camoufox version. - To prevent leaks, only use this for special cases. args (Optional[List[str]]): Arguments to pass to the browser. env (Optional[Dict[str, Union[str, float, bool]]]): Environment variables to set. + persistent_context (Optional[bool]): + Whether to use a persistent context. + debug (Optional[bool]): + Prints the config being sent to Camoufox. **launch_options (Dict[str, Any]): Additional Firefox launch options. """ - data = locals() - data.pop('playwright') + opt = get_launch_options(**_clean_locals(locals())) + + if persistent_context: + return playwright.firefox.launch_persistent_context(**opt) - opt = get_launch_options(**data) return playwright.firefox.launch(**opt) diff --git a/pythonlib/camoufox/utils.py b/pythonlib/camoufox/utils.py index b400a7d..e55bd38 100644 --- a/pythonlib/camoufox/utils.py +++ b/pythonlib/camoufox/utils.py @@ -1,6 +1,8 @@ import os import sys +import warnings from os import environ +from pprint import pprint from random import randrange from typing import Any, Dict, List, Literal, Optional, Tuple, Union, cast @@ -17,7 +19,7 @@ from .addons import ( get_debug_port, threaded_try_load_addons, ) -from .exceptions import InvalidPropertyType, UnknownProperty +from .exceptions import InvalidPropertyType, NonFirefoxFingerprint, UnknownProperty from .fingerprints import from_browserforge, generate_fingerprint from .ip import Proxy, public_ip, valid_ipv4, valid_ipv6 from .locale import geoip_allowed, get_geolocation, normalize_locale @@ -133,7 +135,7 @@ def determine_ua_os(user_agent: str) -> Literal['mac', 'win', 'lin']: parsed_ua = user_agent_parser.ParseOS(user_agent).get('family') if not parsed_ua: raise ValueError("Could not determine OS from user agent") - if parsed_ua.startswith("Mac"): + if parsed_ua.startswith("Mac") or parsed_ua.startswith("iOS"): return "mac" if parsed_ua.startswith("Windows"): return "win" @@ -155,8 +157,8 @@ def get_screen_cons(headless: Optional[bool] = None) -> Optional[Screen]: # Use the dimensions from the monitor with greatest screen real estate monitor = max(monitors, key=lambda m: m.width * m.height) - # Add 25% buffer - return Screen(max_width=int(monitor.width * 1.25), max_height=int(monitor.height * 1.25)) + # Add 15% buffer + return Screen(max_width=int(monitor.width * 1.15), max_height=int(monitor.height * 1.15)) def update_fonts(config: Dict[str, Any], target_os: str) -> None: @@ -173,6 +175,40 @@ def update_fonts(config: Dict[str, Any], target_os: str) -> None: config['fonts'] = fonts +def check_custom_fingerprint(fingerprint: Fingerprint) -> None: + """ + Asserts that the passed BrowserForge fingerprint is a valid Firefox fingerprint. + and warns the user that passing their own fingerprint is not recommended. + """ + if any(browser in fingerprint.navigator.userAgent for browser in ('Firefox', 'FxiOS')): + return + # Tell the user what browser they're using + parsed_ua = user_agent_parser.ParseUserAgent(fingerprint.navigator.userAgent).get( + 'family', 'Non-Firefox' + ) + if parsed_ua: + raise NonFirefoxFingerprint( + f'"{parsed_ua}" fingerprints are not supported in Camoufox. ' + 'Using fingerprints from a browser other than Firefox WILL lead to detection. ' + 'If this is intentional, pass `i_know_what_im_doing=True`.' + ) + + warnings.warn( + 'Passing your own fingerprint is not recommended. ' + 'BrowserForge fingerprints are automatically generated within Camoufox ' + 'based on the provided `os` and `screen` constraints. ' + ) + + +def _clean_locals(data: Dict[str, Any]) -> Dict[str, Any]: + """ + Gets the launch options from the locals of the function. + """ + del data['playwright'] + del data['persistent_context'] + return data + + def merge_into(target: Dict[str, Any], source: Dict[str, Any]) -> None: """ Merges new keys/values from the source dictionary into the target dictionary. @@ -197,6 +233,8 @@ def get_launch_options( config: Optional[Dict[str, Any]] = None, addons: Optional[List[str]] = None, fingerprint: Optional[Fingerprint] = None, + humanize: Optional[Union[bool, float]] = None, + i_know_what_im_doing: Optional[bool] = None, exclude_addons: Optional[List[DefaultAddons]] = None, screen: Optional[Screen] = None, geoip: Optional[Union[str, bool]] = None, @@ -214,6 +252,7 @@ def get_launch_options( headless: Optional[bool] = None, firefox_user_prefs: Optional[Dict[str, Any]] = None, launch_options: Optional[Dict[str, Any]] = None, + debug: Optional[bool] = None, ) -> Dict[str, Any]: """ Builds the launch options for the Camoufox browser. @@ -242,12 +281,18 @@ def get_launch_options( else: ff_version_str = installed_verstr().split('.', 1)[0] - # Inject a unique Firefox fingerprint + # Generate a fingerprint if fingerprint is None: fingerprint = generate_fingerprint( screen=screen or get_screen_cons(headless), os=os, ) + else: + # Or use the one passed by the user + if not i_know_what_im_doing: + check_custom_fingerprint(fingerprint) + + # Inject the fingerprint into the config merge_into( config, from_browserforge(fingerprint, ff_version_str), @@ -290,9 +335,20 @@ def get_launch_options( parsed_locale = normalize_locale(locale) config.update(parsed_locale.as_config()) + # Pass the humanize option + if humanize: + set_into(config, 'humanize', True) + if isinstance(humanize, (int, float)): + set_into(config, 'humanize:maxTime', humanize) + # Validate the config validate_config(config) + # Print the config if debug is enabled + if debug: + print('[DEBUG] Config:') + pprint(config) + # Set Firefox user preferences if block_images: firefox_user_prefs['permissions.default.image'] = 2 diff --git a/pythonlib/pyproject.toml b/pythonlib/pyproject.toml index 668c0fc..2a4a1dd 100644 --- a/pythonlib/pyproject.toml +++ b/pythonlib/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "camoufox" -version = "0.2.4" +version = "0.2.5" description = "Wrapper around Playwright to help launch Camoufox" authors = ["daijro "] license = "MIT"