mirror of
https://github.com/daijro/camoufox.git
synced 2026-08-19 00:00:58 +00:00
784 lines
28 KiB
Python
784 lines
28 KiB
Python
import os
|
|
import sys
|
|
from os import environ
|
|
from os.path import abspath
|
|
from pathlib import Path
|
|
from pprint import pprint
|
|
from random import randint, randrange
|
|
from typing import Any, Dict, List, Literal, Optional, Tuple, Union, cast
|
|
|
|
import numpy as np
|
|
import orjson
|
|
from browserforge.fingerprints import Fingerprint, Screen
|
|
from screeninfo import get_monitors
|
|
from typing_extensions import TypeAlias
|
|
from ua_parser import user_agent_parser
|
|
|
|
from .addons import DefaultAddons, add_default_addons, confirm_paths
|
|
from .exceptions import (
|
|
InvalidOS,
|
|
InvalidPropertyType,
|
|
NonFirefoxFingerprint,
|
|
)
|
|
from .fingerprints import from_browserforge, from_preset, generate_fingerprint, get_random_preset, _generate_random_font_subset, _generate_random_voice_subset
|
|
from .geolocation import geoip_allowed, get_geolocation
|
|
from .ip import Proxy, public_ip, valid_ipv4, valid_ipv6
|
|
from .locales import handle_locales
|
|
from .pkgman import OS_NAME, get_path, installed_verstr, launch_path
|
|
from .virtdisplay import VirtualDisplay
|
|
from ._warnings import LeakWarning
|
|
from .webgl import sample_webgl
|
|
|
|
ListOrString: TypeAlias = Union[Tuple[str, ...], List[str], str]
|
|
|
|
# Camoufox preferences to cache previous pages and requests
|
|
CACHE_PREFS = {
|
|
'browser.sessionhistory.max_entries': 10,
|
|
'browser.sessionhistory.max_total_viewers': -1,
|
|
'browser.cache.memory.enable': True,
|
|
'browser.cache.disk_cache_ssl': True,
|
|
'browser.cache.disk.smart_size.enabled': True,
|
|
}
|
|
|
|
|
|
def _generate_fontconfig(fontconfig_path: str) -> str:
|
|
"""
|
|
Generates a runtime fontconfig that resolves bundled font paths absolutely.
|
|
The bundled fonts.conf uses prefix="cwd" relative paths which break when
|
|
Playwright's working directory differs from the browser install directory.
|
|
Writes a patched copy to ~/.cache/camoufox/fontconfig/ (deterministic,
|
|
only regenerated when content changes).
|
|
"""
|
|
import hashlib
|
|
|
|
fonts_dir = get_path("fonts")
|
|
fonts_conf_src = os.path.join(fontconfig_path, "fonts.conf")
|
|
|
|
with open(fonts_conf_src, 'r') as f:
|
|
conf_content = f.read()
|
|
|
|
conf_content = conf_content.replace(
|
|
'<dir prefix="cwd">fonts</dir>',
|
|
f'<dir>{fonts_dir}</dir>',
|
|
)
|
|
|
|
cache_dir = os.path.join(os.path.expanduser('~'), '.cache', 'camoufox', 'fontconfig')
|
|
os.makedirs(cache_dir, exist_ok=True)
|
|
|
|
content_hash = hashlib.sha256(conf_content.encode()).hexdigest()[:12]
|
|
runtime_conf = os.path.join(cache_dir, f'fonts-{content_hash}.conf')
|
|
if not os.path.exists(runtime_conf):
|
|
with open(runtime_conf, 'w') as f:
|
|
f.write(conf_content)
|
|
|
|
return runtime_conf
|
|
|
|
|
|
def get_env_vars(
|
|
config_map: Dict[str, str], user_agent_os: str
|
|
) -> Dict[str, Union[str, float, bool]]:
|
|
"""
|
|
Gets a dictionary of environment variables for Camoufox.
|
|
"""
|
|
env_vars: Dict[str, Union[str, float, bool]] = {}
|
|
try:
|
|
updated_config_data = orjson.dumps(config_map)
|
|
except orjson.JSONEncodeError as e:
|
|
print(f"Error updating config: {e}")
|
|
sys.exit(1)
|
|
|
|
# Split the config into chunks
|
|
chunk_size = 2047 if OS_NAME == 'win' else 32767
|
|
config_str = updated_config_data.decode('utf-8')
|
|
|
|
for i in range(0, len(config_str), chunk_size):
|
|
chunk = config_str[i : i + chunk_size]
|
|
env_name = f"CAMOU_CONFIG_{(i // chunk_size) + 1}"
|
|
try:
|
|
env_vars[env_name] = chunk
|
|
except Exception as e:
|
|
print(f"Error setting {env_name}: {e}")
|
|
sys.exit(1)
|
|
|
|
if OS_NAME == 'lin':
|
|
# https://github.com/coryking/camoufox/commit/f21eeb2850a74cc104fb57e17e0a2fa27b7a2a28
|
|
# Thanks @coryking
|
|
# the user_agent_os is either 'lin', 'mac', or 'win' but our fontconfig directory is 'linux', 'macos', or 'windows'
|
|
directory_map = {
|
|
'lin': 'linux',
|
|
'mac': 'macos',
|
|
'win': 'windows',
|
|
}
|
|
os_dir = directory_map.get(user_agent_os, user_agent_os)
|
|
fontconfig_path = get_path(os.path.join("fontconfig", os_dir))
|
|
|
|
# assert that fonts.conf exists in the directory
|
|
if not os.path.exists(os.path.join(fontconfig_path, "fonts.conf")):
|
|
# puke violently if fonts.conf doesn't exist!!
|
|
raise FileNotFoundError(
|
|
f"fonts.conf not found in {fontconfig_path}! Something ain't right with your camoufox bundle."
|
|
)
|
|
|
|
env_vars['FONTCONFIG_FILE'] = _generate_fontconfig(fontconfig_path)
|
|
|
|
return env_vars
|
|
|
|
|
|
def _load_properties(path: Optional[Path] = None) -> Dict[str, str]:
|
|
"""
|
|
Loads the properties.json file.
|
|
"""
|
|
if path:
|
|
prop_file = str(path.parent / "properties.json")
|
|
else:
|
|
prop_file = get_path("properties.json")
|
|
with open(prop_file, "rb") as f:
|
|
prop_dict = orjson.loads(f.read())
|
|
|
|
return {prop['property']: prop['type'] for prop in prop_dict}
|
|
|
|
|
|
def validate_config(config_map: Dict[str, str], path: Optional[Path] = None) -> None:
|
|
"""
|
|
Validates the config map.
|
|
"""
|
|
property_types = _load_properties(path=path)
|
|
|
|
for key, value in config_map.items():
|
|
expected_type = property_types.get(key)
|
|
if not expected_type:
|
|
print(f'Skipping unknown patch {key} : {value}')
|
|
continue # Property not supported by this browser version; skip silently
|
|
|
|
if not validate_type(value, expected_type):
|
|
raise InvalidPropertyType(
|
|
f"Invalid type for property {key}. Expected {expected_type}, got {type(value).__name__}"
|
|
)
|
|
|
|
|
|
def validate_type(value: Any, expected_type: str) -> bool:
|
|
"""
|
|
Validates the type of the value.
|
|
"""
|
|
if expected_type == "str":
|
|
return isinstance(value, str)
|
|
elif expected_type == "int":
|
|
return isinstance(value, int) or (isinstance(value, float) and value.is_integer())
|
|
elif expected_type == "uint":
|
|
return (
|
|
isinstance(value, int) or (isinstance(value, float) and value.is_integer())
|
|
) and value >= 0
|
|
elif expected_type == "double":
|
|
return isinstance(value, (float, int))
|
|
elif expected_type == "bool":
|
|
return isinstance(value, bool)
|
|
elif expected_type == "array":
|
|
return isinstance(value, list)
|
|
elif expected_type == "dict":
|
|
return isinstance(value, dict)
|
|
else:
|
|
return False
|
|
|
|
|
|
def get_target_os(config: Dict[str, Any]) -> Literal['mac', 'win', 'lin']:
|
|
"""
|
|
Gets the OS from the config if the user agent is set,
|
|
otherwise returns the OS of the current system.
|
|
"""
|
|
if config.get("navigator.userAgent"):
|
|
return determine_ua_os(config["navigator.userAgent"])
|
|
return OS_NAME
|
|
|
|
|
|
def determine_ua_os(user_agent: str) -> Literal['mac', 'win', 'lin']:
|
|
"""
|
|
Determines the OS from the user agent string.
|
|
"""
|
|
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"):
|
|
return "mac"
|
|
if parsed_ua.startswith("Windows"):
|
|
return "win"
|
|
return "lin"
|
|
|
|
|
|
def get_screen_cons(headless: Optional[bool] = None) -> Optional[Screen]:
|
|
"""
|
|
Determines a sane viewport size for Camoufox if being ran in headful mode.
|
|
"""
|
|
if headless is False:
|
|
return None # Skip if headless
|
|
try:
|
|
monitors = get_monitors()
|
|
except Exception:
|
|
return None # Skip if there's an error getting the monitors
|
|
if not monitors:
|
|
return None # Skip if there are no monitors
|
|
|
|
# Use the dimensions from the monitor with greatest screen real estate
|
|
monitor = max(monitors, key=lambda m: m.width * m.height)
|
|
return Screen(max_width=monitor.width, max_height=monitor.height)
|
|
|
|
|
|
def update_fonts(config: Dict[str, Any], target_os: str) -> None:
|
|
"""
|
|
Updates the fonts for the target OS.
|
|
"""
|
|
with open(os.path.join(os.path.dirname(__file__), "fonts.json"), "rb") as f:
|
|
fonts = orjson.loads(f.read())[target_os]
|
|
|
|
# Merge with existing fonts
|
|
if 'fonts' in config:
|
|
config['fonts'] = np.unique(fonts + config['fonts']).tolist()
|
|
else:
|
|
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.
|
|
"""
|
|
# Check what the browser is
|
|
browser_name = user_agent_parser.ParseUserAgent(fingerprint.navigator.userAgent).get(
|
|
'family', 'Non-Firefox'
|
|
)
|
|
if browser_name != 'Firefox':
|
|
raise NonFirefoxFingerprint(
|
|
f'"{browser_name}" 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`.'
|
|
)
|
|
|
|
LeakWarning.warn('custom_fingerprint', False)
|
|
|
|
|
|
def check_valid_os(os: ListOrString) -> None:
|
|
"""
|
|
Checks if the target OS is valid.
|
|
"""
|
|
if not isinstance(os, str):
|
|
for os_name in os:
|
|
check_valid_os(os_name)
|
|
return
|
|
# Assert that the OS is lowercase
|
|
if not os.islower():
|
|
raise InvalidOS(f"OS values must be lowercase: '{os}'")
|
|
# Assert that the OS is supported by Camoufox
|
|
if os not in ('windows', 'macos', 'linux'):
|
|
raise InvalidOS(f"Camoufox does not support the OS: '{os}'")
|
|
|
|
|
|
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.
|
|
Given that the key does not exist in the target dictionary.
|
|
"""
|
|
for key, value in source.items():
|
|
if key not in target:
|
|
target[key] = value
|
|
|
|
|
|
def set_into(target: Dict[str, Any], key: str, value: Any) -> None:
|
|
"""
|
|
Sets a new key/value into the target dictionary.
|
|
Given that the key does not exist in the target dictionary.
|
|
"""
|
|
if key not in target:
|
|
target[key] = value
|
|
|
|
|
|
def is_domain_set(
|
|
config: Dict[str, Any],
|
|
*properties: str,
|
|
) -> bool:
|
|
"""
|
|
Checks if a domain is set in the config.
|
|
"""
|
|
for prop in properties:
|
|
# If the . prefix exists, check if the domain is a prefix of any key in the config
|
|
if prop[-1] in ('.', ':'):
|
|
if any(key.startswith(prop) for key in config):
|
|
return True
|
|
# Otherwise, check if the domain is a direct key in the config
|
|
else:
|
|
if prop in config:
|
|
return True
|
|
return False
|
|
|
|
|
|
def warn_manual_config(config: Dict[str, Any]) -> None:
|
|
"""
|
|
Warns the user if they are manually setting properties that Camoufox already sets internally.
|
|
"""
|
|
# Manual locale setting
|
|
if is_domain_set(
|
|
config, 'navigator.language', 'navigator.languages', 'headers.Accept-Language', 'locale:'
|
|
):
|
|
LeakWarning.warn('locale', False)
|
|
# Manual geolocation and timezone setting
|
|
if is_domain_set(config, 'geolocation:', 'timezone'):
|
|
LeakWarning.warn('geolocation', False)
|
|
# Manual User-Agent setting
|
|
if is_domain_set(config, 'headers.User-Agent'):
|
|
LeakWarning.warn('header-ua', False)
|
|
# Manual navigator setting
|
|
if is_domain_set(config, 'navigator.'):
|
|
LeakWarning.warn('navigator', False)
|
|
# Manual screen/window setting
|
|
if is_domain_set(config, 'screen.', 'window.', 'document.body.'):
|
|
LeakWarning.warn('viewport', False)
|
|
|
|
|
|
async def async_attach_vd(
|
|
browser: Any, virtual_display: Optional[VirtualDisplay] = None
|
|
) -> Any: # type: ignore
|
|
"""
|
|
Attaches the virtual display to the async browser cleanup
|
|
"""
|
|
if not virtual_display: # Skip if no virtual display is provided
|
|
return browser
|
|
|
|
_close = browser.close
|
|
|
|
async def new_close(*args: Any, **kwargs: Any):
|
|
try:
|
|
await _close(*args, **kwargs)
|
|
except Exception:
|
|
raise
|
|
finally:
|
|
if virtual_display:
|
|
virtual_display.kill()
|
|
|
|
browser.close = new_close
|
|
browser._virtual_display = virtual_display
|
|
|
|
return browser
|
|
|
|
|
|
def sync_attach_vd(
|
|
browser: Any, virtual_display: Optional[VirtualDisplay] = None
|
|
) -> Any: # type: ignore
|
|
"""
|
|
Attaches the virtual display to the sync browser cleanup
|
|
"""
|
|
if not virtual_display: # Skip if no virtual display is provided
|
|
return browser
|
|
|
|
_close = browser.close
|
|
|
|
def new_close(*args: Any, **kwargs: Any):
|
|
try:
|
|
_close(*args, **kwargs)
|
|
except Exception:
|
|
raise
|
|
finally:
|
|
if virtual_display:
|
|
virtual_display.kill()
|
|
|
|
browser.close = new_close
|
|
browser._virtual_display = virtual_display
|
|
|
|
return browser
|
|
|
|
|
|
def launch_options(
|
|
*,
|
|
config: Optional[Dict[str, Any]] = None,
|
|
os: Optional[ListOrString] = None,
|
|
block_images: Optional[bool] = None,
|
|
block_webrtc: Optional[bool] = None,
|
|
block_webgl: Optional[bool] = None,
|
|
disable_coop: Optional[bool] = None,
|
|
webgl_config: Optional[Tuple[str, str]] = None,
|
|
geoip: Optional[Union[str, bool]] = None,
|
|
geoip_db: Optional[str] = None,
|
|
humanize: Optional[Union[bool, float]] = None,
|
|
locale: Optional[Union[str, List[str]]] = None,
|
|
addons: Optional[List[str]] = None,
|
|
fonts: Optional[List[str]] = None,
|
|
custom_fonts_only: Optional[bool] = None,
|
|
exclude_addons: Optional[List[DefaultAddons]] = None,
|
|
screen: Optional[Screen] = None,
|
|
window: Optional[Tuple[int, int]] = None,
|
|
fingerprint: Optional[Fingerprint] = None,
|
|
fingerprint_preset: Optional[Union[bool, Dict[str, Any]]] = None,
|
|
ff_version: Optional[int] = None,
|
|
headless: Optional[bool] = None,
|
|
main_world_eval: Optional[bool] = None,
|
|
executable_path: Optional[Union[str, Path]] = None,
|
|
browser: Optional[str] = None,
|
|
firefox_user_prefs: Optional[Dict[str, Any]] = None,
|
|
proxy: Optional[Dict[str, str]] = None,
|
|
enable_cache: Optional[bool] = None,
|
|
args: Optional[List[str]] = None,
|
|
env: Optional[Dict[str, Union[str, float, bool]]] = None,
|
|
i_know_what_im_doing: Optional[bool] = None,
|
|
debug: Optional[bool] = None,
|
|
virtual_display: Optional[str] = None,
|
|
**launch_options: Dict[str, Any],
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Launches a new browser instance for Camoufox.
|
|
Accepts all Playwright Firefox launch options, along with the following:
|
|
|
|
Parameters:
|
|
config (Optional[Dict[str, Any]]):
|
|
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", "linux", or a list to randomly choose from.
|
|
Default: ["windows", "macos", "linux"]
|
|
block_images (Optional[bool]):
|
|
Whether to block all images.
|
|
block_webrtc (Optional[bool]):
|
|
Whether to block WebRTC entirely.
|
|
block_webgl (Optional[bool]):
|
|
Whether to block WebGL. To prevent leaks, only use this for special cases.
|
|
disable_coop (Optional[bool]):
|
|
Disables the Cross-Origin-Opener-Policy, allowing elements in cross-origin iframes,
|
|
such as the Turnstile checkbox, to be clicked.
|
|
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.
|
|
geoip_db (Optional[str]):
|
|
Name of the GeoIP database to use (e.g., "MaxMind").
|
|
If not specified, uses the configured default.
|
|
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[Union[str, List[str]]]):
|
|
Locale(s) to use in Camoufox. The first listed locale will be used for the Intl API.
|
|
addons (Optional[List[str]]):
|
|
List of Firefox addons to use.
|
|
fonts (Optional[List[str]]):
|
|
Fonts to load into Camoufox (in addition to the default fonts for the target `os`).
|
|
Takes a list of font family names that are installed on the system.
|
|
custom_fonts_only (Optional[bool]):
|
|
If enabled, OS-specific system fonts will be not be passed to Camoufox.
|
|
exclude_addons (Optional[List[DefaultAddons]]):
|
|
Default addons to exclude. Passed as a list of camoufox.DefaultAddons enums.
|
|
screen (Optional[Screen]):
|
|
Constrains the screen dimensions of the generated fingerprint.
|
|
Takes a browserforge.fingerprints.Screen instance.
|
|
window (Optional[Tuple[int, int]]):
|
|
Set a fixed window size instead of generating a random one
|
|
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.
|
|
fingerprint_preset (Optional[Union[bool, Dict[str, Any]]]):
|
|
Opt into using real fingerprint presets instead of BrowserForge.
|
|
Pass `True` to use a random bundled preset, or pass a preset dict directly.
|
|
By default (None), BrowserForge is used for infinite unique fingerprints.
|
|
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 False.
|
|
Note: If you are running linux, passing headless='virtual' to Camoufox & AsyncCamoufox
|
|
will use Xvfb.
|
|
main_world_eval (Optional[bool]):
|
|
Whether to enable running scripts in the main world.
|
|
To use this, prepend "mw:" to the script: page.evaluate("mw:" + script).
|
|
executable_path (Optional[Union[str, Path]]):
|
|
Custom Camoufox browser executable path.
|
|
browser (Optional[str]):
|
|
Select a specific installed browser version. Can be:
|
|
- Repo/build like "official/beta.20"
|
|
- Build alone like "beta.20"
|
|
- Full version like "134.0.2-beta.20"
|
|
If not specified, uses the active version.
|
|
firefox_user_prefs (Optional[Dict[str, Any]]):
|
|
Firefox user preferences to set.
|
|
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.
|
|
enable_cache (Optional[bool]):
|
|
Cache previous pages, requests, etc (uses more memory).
|
|
args (Optional[List[str]]):
|
|
Arguments to pass to the browser.
|
|
env (Optional[Dict[str, Union[str, float, bool]]]):
|
|
Environment variables to set.
|
|
debug (Optional[bool]):
|
|
Prints the config being sent to Camoufox.
|
|
virtual_display (Optional[str]):
|
|
Virtual display number. Ex: ':99'. This is handled by Camoufox & AsyncCamoufox.
|
|
webgl_config (Optional[Tuple[str, str]]):
|
|
Use a specific WebGL vendor/renderer pair. Passed as a tuple of (vendor, renderer).
|
|
**launch_options (Dict[str, Any]):
|
|
Additional Firefox launch options.
|
|
"""
|
|
# Build the config
|
|
if config is None:
|
|
config = {}
|
|
|
|
# Set default values for optional arguments
|
|
if headless is None:
|
|
headless = False
|
|
if addons is None:
|
|
addons = []
|
|
if args is None:
|
|
args = []
|
|
if firefox_user_prefs is None:
|
|
firefox_user_prefs = {}
|
|
if custom_fonts_only is None:
|
|
custom_fonts_only = False
|
|
if i_know_what_im_doing is None:
|
|
i_know_what_im_doing = False
|
|
if env is None:
|
|
env = cast(Dict[str, Union[str, float, bool]], environ)
|
|
if isinstance(executable_path, str):
|
|
# Convert executable path to a Path object
|
|
executable_path = Path(abspath(executable_path))
|
|
|
|
# Handle virtual display
|
|
if virtual_display:
|
|
env['DISPLAY'] = virtual_display
|
|
# Virtual display uses Xvfb (X11). If the host session forces Wayland via env vars,
|
|
# GTK/Firefox may try Wayland and ignore DISPLAY, breaking Xvfb usage.
|
|
env['GDK_BACKEND'] = 'x11'
|
|
env.pop('WAYLAND_DISPLAY', None)
|
|
env["MOZ_ENABLE_WAYLAND"] = "0"
|
|
|
|
# Warn the user for manual config settings
|
|
if not i_know_what_im_doing:
|
|
warn_manual_config(config)
|
|
|
|
# Assert the target OS is valid
|
|
if os:
|
|
check_valid_os(os)
|
|
|
|
# webgl_config requires OS to be set
|
|
elif webgl_config:
|
|
raise ValueError('OS must be set when using webgl_config')
|
|
|
|
# Add the default addons
|
|
add_default_addons(addons, exclude_addons)
|
|
|
|
# Confirm all addon paths are valid
|
|
if addons:
|
|
confirm_paths(addons)
|
|
config['addons'] = addons
|
|
|
|
# Get the Firefox version
|
|
if ff_version:
|
|
ff_version_str = str(ff_version)
|
|
LeakWarning.warn('ff_version', i_know_what_im_doing)
|
|
else:
|
|
ff_version_str = installed_verstr().split('.', 1)[0]
|
|
|
|
# Generate a fingerprint
|
|
_used_preset = False
|
|
if fingerprint is not None:
|
|
# User passed a custom BrowserForge fingerprint
|
|
if not i_know_what_im_doing:
|
|
check_custom_fingerprint(fingerprint)
|
|
elif fingerprint_preset is not None:
|
|
# User opted into real fingerprint presets
|
|
if isinstance(fingerprint_preset, dict):
|
|
preset = fingerprint_preset
|
|
else:
|
|
preset = get_random_preset(os=os, ff_version=ff_version_str)
|
|
if preset:
|
|
merge_into(config, from_preset(preset, ff_version_str))
|
|
_used_preset = True
|
|
|
|
if not _used_preset and fingerprint is None:
|
|
# Default: BrowserForge synthetic generation (infinite unique fingerprints)
|
|
fingerprint = generate_fingerprint(
|
|
screen=screen or get_screen_cons(headless or 'DISPLAY' in env),
|
|
window=window,
|
|
os=os,
|
|
)
|
|
|
|
if not _used_preset and fingerprint is not None:
|
|
# Inject the BrowserForge fingerprint into the config
|
|
merge_into(
|
|
config,
|
|
from_browserforge(fingerprint, ff_version_str),
|
|
)
|
|
|
|
target_os = get_target_os(config)
|
|
|
|
# Set a random window.history.length
|
|
set_into(config, 'window.history.length', randrange(1, 6)) # nosec
|
|
|
|
# Update fonts list
|
|
if fonts:
|
|
config['fonts'] = fonts
|
|
|
|
if custom_fonts_only:
|
|
firefox_user_prefs['gfx.bundled-fonts.activate'] = 0
|
|
if fonts:
|
|
LeakWarning.warn('custom_fonts_only')
|
|
else:
|
|
raise ValueError('No custom fonts were passed, but `custom_fonts_only` is enabled.')
|
|
elif 'fonts' not in config or not config.get('fonts'):
|
|
# Generate a unique random font subset from the OS font list
|
|
os_name = {'win': 'windows', 'mac': 'macos', 'lin': 'linux'}.get(target_os, 'macos')
|
|
try:
|
|
config['fonts'] = _generate_random_font_subset(os_name)
|
|
except Exception:
|
|
update_fonts(config, target_os)
|
|
|
|
# Generate a unique random voice subset
|
|
if 'voices' not in config:
|
|
os_name_v = {'win': 'windows', 'mac': 'macos', 'lin': 'linux'}.get(target_os, 'macos')
|
|
try:
|
|
config['voices'] = _generate_random_voice_subset(os_name_v)
|
|
except Exception:
|
|
pass
|
|
|
|
# Set random seeds for fingerprint noise (per launch)
|
|
set_into(config, 'fonts:spacing_seed', randint(1, 4_294_967_295)) # nosec
|
|
set_into(config, 'audio:seed', randint(1, 4_294_967_295)) # nosec
|
|
set_into(config, 'canvas:seed', randint(1, 4_294_967_295)) # nosec
|
|
|
|
# Set geolocation
|
|
if geoip:
|
|
geoip_allowed() # Assert that geoip is allowed
|
|
|
|
if geoip is True:
|
|
# Find the user's IP address
|
|
if proxy:
|
|
geoip = public_ip(Proxy(**proxy).as_string())
|
|
else:
|
|
geoip = public_ip()
|
|
|
|
# Spoof WebRTC if not blocked
|
|
if not block_webrtc:
|
|
if valid_ipv4(geoip):
|
|
set_into(config, 'webrtc:ipv4', geoip)
|
|
firefox_user_prefs['network.dns.disableIPv6'] = True
|
|
elif valid_ipv6(geoip):
|
|
set_into(config, 'webrtc:ipv6', geoip)
|
|
|
|
geolocation = get_geolocation(geoip, geoip_db=geoip_db)
|
|
geo_config = geolocation.as_config()
|
|
for key, value in geo_config.items():
|
|
if key in ('timezone', 'locale:language', 'locale:region', 'locale:script'):
|
|
config.setdefault(key, value)
|
|
else:
|
|
config[key] = value
|
|
|
|
# Raise a warning when a proxy is being used without spoofing geolocation.
|
|
# This is a very bad idea; the warning cannot be ignored with i_know_what_im_doing.
|
|
elif (
|
|
proxy
|
|
and 'localhost' not in proxy.get('server', '')
|
|
and not is_domain_set(config, 'geolocation')
|
|
):
|
|
LeakWarning.warn('proxy_without_geoip')
|
|
|
|
# Set locale
|
|
if locale:
|
|
handle_locales(locale, config)
|
|
|
|
# Pass the humanize option
|
|
if humanize:
|
|
set_into(config, 'humanize', True)
|
|
if isinstance(humanize, (int, float)):
|
|
set_into(config, 'humanize:maxTime', humanize)
|
|
|
|
# Enable the main world context creation
|
|
if main_world_eval:
|
|
set_into(config, 'allowMainWorld', True)
|
|
|
|
# Set Firefox user preferences
|
|
if block_images:
|
|
LeakWarning.warn('block_images', i_know_what_im_doing)
|
|
firefox_user_prefs['permissions.default.image'] = 2
|
|
if block_webrtc:
|
|
firefox_user_prefs['media.peerconnection.enabled'] = False
|
|
if disable_coop:
|
|
LeakWarning.warn('disable_coop', i_know_what_im_doing)
|
|
firefox_user_prefs['browser.tabs.remote.useCrossOriginOpenerPolicy'] = False
|
|
|
|
# Allow allow_webgl parameter for backwards compatibility
|
|
if block_webgl or launch_options.pop('allow_webgl', True) is False:
|
|
firefox_user_prefs['webgl.disabled'] = True
|
|
LeakWarning.warn('block_webgl', i_know_what_im_doing)
|
|
else:
|
|
# If the user has provided a specific WebGL vendor/renderer pair, use it
|
|
if webgl_config:
|
|
webgl_fp = sample_webgl(target_os, *webgl_config)
|
|
elif config.get('webGl:vendor') and config.get('webGl:renderer'):
|
|
# Preset already set vendor/renderer — sample matching WebGL params
|
|
webgl_fp = sample_webgl(target_os, config['webGl:vendor'], config['webGl:renderer'])
|
|
else:
|
|
webgl_fp = sample_webgl(target_os)
|
|
enable_webgl2 = webgl_fp.pop('webGl2Enabled')
|
|
|
|
# Merge the WebGL fingerprint into the config
|
|
merge_into(config, webgl_fp)
|
|
# Set the WebGL preferences
|
|
merge_into(
|
|
firefox_user_prefs,
|
|
{
|
|
'webgl.enable-webgl2': enable_webgl2,
|
|
'webgl.force-enabled': True,
|
|
},
|
|
)
|
|
|
|
# Cache previous pages, requests, etc (uses more memory)
|
|
if enable_cache:
|
|
merge_into(firefox_user_prefs, CACHE_PREFS)
|
|
|
|
# Print the config if debug is enabled
|
|
if debug:
|
|
print('[DEBUG] Config:')
|
|
pprint(config)
|
|
|
|
# Validate the config
|
|
validate_config(config, path=executable_path)
|
|
|
|
# Prepare environment variables to pass to Camoufox
|
|
env_vars = {
|
|
**get_env_vars(config, target_os),
|
|
**env,
|
|
}
|
|
# Prepare the executable path
|
|
if executable_path:
|
|
executable_path = str(executable_path)
|
|
elif browser:
|
|
# Select a specific installed browser version
|
|
from .multiversion import find_installed_version
|
|
|
|
browser_path = find_installed_version(browser)
|
|
if not browser_path:
|
|
raise ValueError(
|
|
f"Browser version '{browser}' not found. Run `camoufox list` to see installed versions."
|
|
)
|
|
executable_path = launch_path(browser_path)
|
|
else:
|
|
executable_path = launch_path()
|
|
|
|
result = {
|
|
"executable_path": executable_path,
|
|
"args": args,
|
|
"env": env_vars,
|
|
"firefox_user_prefs": firefox_user_prefs,
|
|
"headless": headless,
|
|
**(launch_options if launch_options is not None else {}),
|
|
}
|
|
# Only include proxy if it's not None (Playwright 1.55+ validates this)
|
|
# https://github.com/coryking/camoufox/commit/1336e8e509e8c12a896a09d9ee51f131f739f106
|
|
# Thanks @coryking
|
|
if proxy is not None:
|
|
result["proxy"] = proxy
|
|
|
|
return result
|