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
This commit is contained in:
daijro 2024-10-04 18:59:04 -05:00
parent 2201d9fc0c
commit 2832673f83
6 changed files with 188 additions and 49 deletions

View file

@ -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.
<details>
<summary>Injecting custom Fingerprint objects...</summary>
> [!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/")
```
<hr width=50>
**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.
</details>
---

View file

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

View file

@ -100,3 +100,11 @@ class NotInstalledGeoIPExtra(ImportError):
"""
...
class NonFirefoxFingerprint(Exception):
"""
Raised when a passed Browserforge fingerprint is invalid.
"""
...

View file

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

View file

@ -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

View file

@ -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 <daijro.dev@gmail.com>"]
license = "MIT"