pythonlib: Implement viewport hijacking 0.2.2

- Allow Browerforge fingerprints to override screen & viewport data.
- Set Camoufox's window dimensions to a fixed width/height generated from Browserforge.
- If the browser is headful, do not exceed 125% of the screen size.

Other changes:
- Allow public IP finder to work without verification.
- Add script to publish to pypi.
- More descriptive progress bars when downloading default addons.
- Bump to 0.2.2
This commit is contained in:
daijro 2024-09-30 04:06:16 -05:00
parent b371928d43
commit ad55622550
11 changed files with 100 additions and 48 deletions

View file

@ -114,8 +114,10 @@ Parameters:
Use a custom BrowserForge fingerprint. Note: Not all values will be implemented. 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. If not provided, a random fingerprint will be generated based on the provided os & user_agent.
screen (Optional[Screen]): screen (Optional[Screen]):
NOT YET IMPLEMENTED: Constrains the screen dimensions of the generated fingerprint. Constrains the screen dimensions of the generated fingerprint.
Takes a browserforge.fingerprints.Screen instance. Takes a browserforge.fingerprints.Screen instance.
headless (Optional[bool]):
Whether to run the browser in headless mode. Defaults to True.
executable_path (Optional[str]): executable_path (Optional[str]):
Custom Camoufox browser executable path. Custom Camoufox browser executable path.
firefox_user_prefs (Optional[Dict[str, Any]]): firefox_user_prefs (Optional[Dict[str, Any]]):

View file

@ -16,7 +16,7 @@ class DefaultAddons(Enum):
Default addons to be downloaded Default addons to be downloaded
""" """
uBO = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi" UBO = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi"
BPC = "https://gitflic.ru/project/magnolia1234/bpc_uploads/blob/raw?file=bypass_paywalls_clean-latest.xpi" BPC = "https://gitflic.ru/project/magnolia1234/bpc_uploads/blob/raw?file=bypass_paywalls_clean-latest.xpi"

View file

@ -44,6 +44,7 @@ async def AsyncNewBrowser(
exclude_addons: Optional[List[DefaultAddons]] = None, exclude_addons: Optional[List[DefaultAddons]] = None,
fingerprint: Optional[Fingerprint] = None, fingerprint: Optional[Fingerprint] = None,
screen: Optional[Screen] = None, screen: Optional[Screen] = None,
headless: Optional[bool] = None,
executable_path: Optional[str] = None, executable_path: Optional[str] = None,
firefox_user_prefs: Optional[Dict[str, Any]] = None, firefox_user_prefs: Optional[Dict[str, Any]] = None,
proxy: Optional[Dict[str, str]] = None, proxy: Optional[Dict[str, str]] = None,
@ -85,8 +86,10 @@ async def AsyncNewBrowser(
Use a custom BrowserForge fingerprint. Note: Not all values will be implemented. 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. If not provided, a random fingerprint will be generated based on the provided os & user_agent.
screen (Optional[Screen]): screen (Optional[Screen]):
NOT YET IMPLEMENTED: Constrains the screen dimensions of the generated fingerprint. Constrains the screen dimensions of the generated fingerprint.
Takes a browserforge.fingerprints.Screen instance. Takes a browserforge.fingerprints.Screen instance.
headless (Optional[bool]):
Whether to run the browser in headless mode. Defaults to True.
executable_path (Optional[str]): executable_path (Optional[str]):
Custom Camoufox browser executable path. Custom Camoufox browser executable path.
firefox_user_prefs (Optional[Dict[str, Any]]): firefox_user_prefs (Optional[Dict[str, Any]]):

View file

@ -28,23 +28,21 @@ navigator:
screen: screen:
# hasHDR is not implemented in Camoufox # hasHDR is not implemented in Camoufox
# Screen size values seem to be inconsistent, and will not be implemented for the time being. availLeft: screen.availLeft
# availHeight: screen.availHeight availTop: screen.availTop
# availWidth: screen.availWidth availWidth: screen.availWidth
# availTop: screen.availTop availHeight: screen.availHeight
# availLeft: screen.availLeft height: screen.height
# height: screen.height width: screen.width
# width: screen.width
colorDepth: screen.colorDepth colorDepth: screen.colorDepth
pixelDepth: screen.pixelDepth pixelDepth: screen.pixelDepth
# devicePixelRatio is not recommended. Any value other than 1.0 is suspicious. # devicePixelRatio is not recommended. Any value other than 1.0 is suspicious.
pageXOffset: screen.pageXOffset pageXOffset: screen.pageXOffset
pageYOffset: screen.pageYOffset pageYOffset: screen.pageYOffset
# Disable viewport hijacking temporarily. outerHeight: window.outerHeight
# outerHeight: window.outerHeight outerWidth: window.outerWidth
# outerWidth: window.outerWidth innerHeight: window.innerHeight
# innerHeight: window.innerHeight innerWidth: window.innerWidth
# innerWidth: window.innerWidth
screenX: window.screenX screenX: window.screenX
# Tends to generate out of bounds (network inconsistencies): # Tends to generate out of bounds (network inconsistencies):
# clientWidth: document.body.clientWidth # clientWidth: document.body.clientWidth

View file

@ -1,7 +1,7 @@
import os.path import os.path
import re import re
from dataclasses import asdict from dataclasses import asdict
from typing import Optional from typing import Any, Dict, Optional
from browserforge.fingerprints import Fingerprint, FingerprintGenerator from browserforge.fingerprints import Fingerprint, FingerprintGenerator
from yaml import CLoader, load from yaml import CLoader, load
@ -40,8 +40,11 @@ def _cast_to_properties(
camoufox_data[type_key] = data camoufox_data[type_key] = data
def from_browserforge(fingerprint: Fingerprint, ff_version: Optional[str] = None) -> dict: def from_browserforge(fingerprint: Fingerprint, ff_version: Optional[str] = None) -> Dict[str, Any]:
camoufox_data = {} """
Converts a Browserforge fingerprint to a Camoufox config.
"""
camoufox_data: Dict[str, Any] = {}
_cast_to_properties( _cast_to_properties(
camoufox_data, camoufox_data,
cast_enum=BROWSERFORGE_DATA, cast_enum=BROWSERFORGE_DATA,
@ -51,15 +54,15 @@ def from_browserforge(fingerprint: Fingerprint, ff_version: Optional[str] = None
return camoufox_data return camoufox_data
def generate(ff_version: Optional[str] = None, **config) -> dict: def generate_fingerprint(**config) -> Fingerprint:
""" """
Generates a Firefox fingerprint. Generates a Firefox fingerprint with Browserforge.
""" """
data = FP_GENERATOR.generate(**config) return FP_GENERATOR.generate(**config)
return from_browserforge(data, ff_version=ff_version)
if __name__ == "__main__": if __name__ == "__main__":
from pprint import pprint from pprint import pprint
pprint(generate()) fp = generate_fingerprint()
pprint(from_browserforge(fp))

View file

@ -1,9 +1,13 @@
import re import re
import warnings
from contextlib import contextmanager
from dataclasses import dataclass from dataclasses import dataclass
from functools import lru_cache from functools import lru_cache
from typing import Dict, Optional, Tuple from typing import Dict, Optional, Tuple
import requests import requests
from urllib3 import disable_warnings
from urllib3.exceptions import InsecureRequestWarning
from .exceptions import InvalidIP, InvalidProxy from .exceptions import InvalidIP, InvalidProxy
@ -69,6 +73,13 @@ def validate_ip(ip: str) -> None:
raise InvalidIP(f"Invalid IP address: {ip}") raise InvalidIP(f"Invalid IP address: {ip}")
@contextmanager
def _suppress_insecure_warning():
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
yield
@lru_cache(maxsize=None) @lru_cache(maxsize=None)
def public_ip(proxy: Optional[str] = None) -> str: def public_ip(proxy: Optional[str] = None) -> str:
""" """
@ -86,11 +97,13 @@ def public_ip(proxy: Optional[str] = None) -> str:
] ]
for url in URLS: for url in URLS:
try: try:
resp = requests.get( with _suppress_insecure_warning():
url, resp = requests.get( # nosec
proxies=Proxy.as_requests_proxy(proxy) if proxy else None, url,
timeout=5, proxies=Proxy.as_requests_proxy(proxy) if proxy else None,
) timeout=5,
verify=False,
)
resp.raise_for_status() resp.raise_for_status()
ip = resp.text.strip() ip = resp.text.strip()
validate_ip(ip) validate_ip(ip)

View file

@ -44,6 +44,7 @@ def NewBrowser(
exclude_addons: Optional[List[DefaultAddons]] = None, exclude_addons: Optional[List[DefaultAddons]] = None,
fingerprint: Optional[Fingerprint] = None, fingerprint: Optional[Fingerprint] = None,
screen: Optional[Screen] = None, screen: Optional[Screen] = None,
headless: Optional[bool] = None,
executable_path: Optional[str] = None, executable_path: Optional[str] = None,
firefox_user_prefs: Optional[Dict[str, Any]] = None, firefox_user_prefs: Optional[Dict[str, Any]] = None,
proxy: Optional[Dict[str, str]] = None, proxy: Optional[Dict[str, str]] = None,
@ -85,8 +86,10 @@ def NewBrowser(
Use a custom BrowserForge fingerprint. Note: Not all values will be implemented. 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. If not provided, a random fingerprint will be generated based on the provided os & user_agent.
screen (Optional[Screen]): screen (Optional[Screen]):
NOT YET IMPLEMENTED: Constrains the screen dimensions of the generated fingerprint. Constrains the screen dimensions of the generated fingerprint.
Takes a browserforge.fingerprints.Screen instance. Takes a browserforge.fingerprints.Screen instance.
headless (Optional[bool]):
Whether to run the browser in headless mode. Defaults to True.
executable_path (Optional[str]): executable_path (Optional[str]):
Custom Camoufox browser executable path. Custom Camoufox browser executable path.
firefox_user_prefs (Optional[Dict[str, Any]]): firefox_user_prefs (Optional[Dict[str, Any]]):

View file

@ -7,6 +7,7 @@ from typing import Any, Dict, List, Literal, Optional, Tuple, Union, cast
import numpy as np import numpy as np
import orjson import orjson
from browserforge.fingerprints import Fingerprint, Screen from browserforge.fingerprints import Fingerprint, Screen
from screeninfo import get_monitors
from typing_extensions import TypeAlias from typing_extensions import TypeAlias
from ua_parser import user_agent_parser from ua_parser import user_agent_parser
@ -17,7 +18,7 @@ from .addons import (
threaded_try_load_addons, threaded_try_load_addons,
) )
from .exceptions import InvalidPropertyType, UnknownProperty from .exceptions import InvalidPropertyType, UnknownProperty
from .fingerprints import from_browserforge, generate from .fingerprints import from_browserforge, generate_fingerprint
from .ip import Proxy, public_ip, valid_ipv4, valid_ipv6 from .ip import Proxy, public_ip, valid_ipv4, valid_ipv6
from .locale import geoip_allowed, get_geolocation, normalize_locale from .locale import geoip_allowed, get_geolocation, normalize_locale
from .pkgman import OS_NAME, get_path, installed_verstr from .pkgman import OS_NAME, get_path, installed_verstr
@ -139,6 +140,25 @@ def determine_ua_os(user_agent: str) -> Literal['mac', 'win', 'lin']:
return "lin" 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)
# Add 25% buffer
return Screen(max_width=int(monitor.width * 1.25), max_height=int(monitor.height * 1.25))
def update_fonts(config: Dict[str, Any], target_os: str) -> None: def update_fonts(config: Dict[str, Any], target_os: str) -> None:
""" """
Updates the fonts for the target OS. Updates the fonts for the target OS.
@ -191,6 +211,7 @@ def get_launch_options(
allow_webgl: Optional[bool] = None, allow_webgl: Optional[bool] = None,
proxy: Optional[Dict[str, str]] = None, proxy: Optional[Dict[str, str]] = None,
ff_version: Optional[int] = None, ff_version: Optional[int] = None,
headless: Optional[bool] = None,
firefox_user_prefs: Optional[Dict[str, Any]] = None, firefox_user_prefs: Optional[Dict[str, Any]] = None,
launch_options: Optional[Dict[str, Any]] = None, launch_options: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -221,21 +242,17 @@ def get_launch_options(
else: else:
ff_version_str = installed_verstr().split('.', 1)[0] ff_version_str = installed_verstr().split('.', 1)[0]
# Generate new fingerprint # Inject a unique Firefox fingerprint
if fingerprint is None: if fingerprint is None:
merge_into( fingerprint = generate_fingerprint(
config, screen=screen or get_screen_cons(headless),
generate( os=os,
ff_version=ff_version_str,
screen=screen,
os=os,
),
)
else:
merge_into(
config,
from_browserforge(fingerprint, ff_version_str),
) )
merge_into(
config,
from_browserforge(fingerprint, ff_version_str),
)
target_os = get_target_os(config) target_os = get_target_os(config)
# Set a random window.history.length # Set a random window.history.length
@ -296,5 +313,6 @@ def get_launch_options(
"env": env_vars, "env": env_vars,
"firefox_user_prefs": firefox_user_prefs, "firefox_user_prefs": firefox_user_prefs,
"proxy": proxy, "proxy": proxy,
"headless": headless,
**(launch_options if launch_options is not None else {}), **(launch_options if launch_options is not None else {}),
} }

View file

@ -20,13 +20,13 @@ def add_default_addons(
maybe_download_addons(addons, addons_list) maybe_download_addons(addons, addons_list)
def download_and_extract(url: str, extract_path: str) -> None: def download_and_extract(url: str, extract_path: str, name: str) -> None:
""" """
Downloads and extracts an addon from a given URL to a specified path Downloads and extracts an addon from a given URL to a specified path
""" """
# Create a temporary file to store the downloaded zip # Create a temporary file to store the downloaded zip
buffer = webdl(url, desc="Downloading addon") buffer = webdl(url, desc=f"Downloading addon ({name})")
unzip(buffer, extract_path) unzip(buffer, extract_path, f"Extracting addon ({name})")
def get_addon_path(addon_name: str) -> str: def get_addon_path(addon_name: str) -> str:
@ -54,7 +54,7 @@ def maybe_download_addons(addons: List[DefaultAddons], addons_list: List[str]) -
# Addon doesn't exist, create directory and download # Addon doesn't exist, create directory and download
try: try:
os.makedirs(addon_path, exist_ok=True) os.makedirs(addon_path, exist_ok=True)
download_and_extract(addon.value, addon_path) download_and_extract(addon.value, addon_path, addon.name)
# Add the new addon directory path to addons_list # Add the new addon directory path to addons_list
addons_list.append(addon_path) addons_list.append(addon_path)
except Exception as e: except Exception as e:

11
pythonlib/publish.sh Normal file
View file

@ -0,0 +1,11 @@
rm -rf ./dist
rm -rf ./camoufox/*.mmdb
python -m build
twine check dist/*
read -p "Confirm publish? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
twine upload dist/*
fi

View file

@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry] [tool.poetry]
name = "camoufox" name = "camoufox"
version = "0.2.1" version = "0.2.2"
description = "Wrapper around Playwright to help launch Camoufox" description = "Wrapper around Playwright to help launch Camoufox"
authors = ["daijro <daijro.dev@gmail.com>"] authors = ["daijro <daijro.dev@gmail.com>"]
license = "MIT" license = "MIT"
@ -38,6 +38,7 @@ tqdm = "*"
numpy = "*" numpy = "*"
ua_parser = "*" ua_parser = "*"
typing_extensions = "*" typing_extensions = "*"
screeninfo = "*"
lxml = "*" lxml = "*"
language-tags = "*" language-tags = "*"
geoip2 = {version = "*", optional = true} geoip2 = {version = "*", optional = true}