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.
If not provided, a random fingerprint will be generated based on the provided os & user_agent.
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.
headless (Optional[bool]):
Whether to run the browser in headless mode. Defaults to True.
executable_path (Optional[str]):
Custom Camoufox browser executable path.
firefox_user_prefs (Optional[Dict[str, Any]]):

View file

@ -16,7 +16,7 @@ class DefaultAddons(Enum):
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"

View file

@ -44,6 +44,7 @@ async def AsyncNewBrowser(
exclude_addons: Optional[List[DefaultAddons]] = None,
fingerprint: Optional[Fingerprint] = None,
screen: Optional[Screen] = None,
headless: Optional[bool] = None,
executable_path: Optional[str] = None,
firefox_user_prefs: Optional[Dict[str, Any]] = 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.
If not provided, a random fingerprint will be generated based on the provided os & user_agent.
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.
headless (Optional[bool]):
Whether to run the browser in headless mode. Defaults to True.
executable_path (Optional[str]):
Custom Camoufox browser executable path.
firefox_user_prefs (Optional[Dict[str, Any]]):

View file

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

View file

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

View file

@ -1,9 +1,13 @@
import re
import warnings
from contextlib import contextmanager
from dataclasses import dataclass
from functools import lru_cache
from typing import Dict, Optional, Tuple
import requests
from urllib3 import disable_warnings
from urllib3.exceptions import InsecureRequestWarning
from .exceptions import InvalidIP, InvalidProxy
@ -69,6 +73,13 @@ def validate_ip(ip: str) -> None:
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)
def public_ip(proxy: Optional[str] = None) -> str:
"""
@ -86,11 +97,13 @@ def public_ip(proxy: Optional[str] = None) -> str:
]
for url in URLS:
try:
resp = requests.get(
url,
proxies=Proxy.as_requests_proxy(proxy) if proxy else None,
timeout=5,
)
with _suppress_insecure_warning():
resp = requests.get( # nosec
url,
proxies=Proxy.as_requests_proxy(proxy) if proxy else None,
timeout=5,
verify=False,
)
resp.raise_for_status()
ip = resp.text.strip()
validate_ip(ip)

View file

@ -44,6 +44,7 @@ def NewBrowser(
exclude_addons: Optional[List[DefaultAddons]] = None,
fingerprint: Optional[Fingerprint] = None,
screen: Optional[Screen] = None,
headless: Optional[bool] = None,
executable_path: Optional[str] = None,
firefox_user_prefs: Optional[Dict[str, Any]] = 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.
If not provided, a random fingerprint will be generated based on the provided os & user_agent.
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.
headless (Optional[bool]):
Whether to run the browser in headless mode. Defaults to True.
executable_path (Optional[str]):
Custom Camoufox browser executable path.
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 orjson
from browserforge.fingerprints import Fingerprint, Screen
from screeninfo import get_monitors
from typing_extensions import TypeAlias
from ua_parser import user_agent_parser
@ -17,7 +18,7 @@ from .addons import (
threaded_try_load_addons,
)
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 .locale import geoip_allowed, get_geolocation, normalize_locale
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"
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:
"""
Updates the fonts for the target OS.
@ -191,6 +211,7 @@ def get_launch_options(
allow_webgl: Optional[bool] = None,
proxy: Optional[Dict[str, str]] = None,
ff_version: Optional[int] = None,
headless: Optional[bool] = None,
firefox_user_prefs: Optional[Dict[str, Any]] = None,
launch_options: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
@ -221,21 +242,17 @@ def get_launch_options(
else:
ff_version_str = installed_verstr().split('.', 1)[0]
# Generate new fingerprint
# Inject a unique Firefox fingerprint
if fingerprint is None:
merge_into(
config,
generate(
ff_version=ff_version_str,
screen=screen,
os=os,
),
)
else:
merge_into(
config,
from_browserforge(fingerprint, ff_version_str),
fingerprint = generate_fingerprint(
screen=screen or get_screen_cons(headless),
os=os,
)
merge_into(
config,
from_browserforge(fingerprint, ff_version_str),
)
target_os = get_target_os(config)
# Set a random window.history.length
@ -296,5 +313,6 @@ def get_launch_options(
"env": env_vars,
"firefox_user_prefs": firefox_user_prefs,
"proxy": proxy,
"headless": headless,
**(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)
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
"""
# Create a temporary file to store the downloaded zip
buffer = webdl(url, desc="Downloading addon")
unzip(buffer, extract_path)
buffer = webdl(url, desc=f"Downloading addon ({name})")
unzip(buffer, extract_path, f"Extracting addon ({name})")
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
try:
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
addons_list.append(addon_path)
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]
name = "camoufox"
version = "0.2.1"
version = "0.2.2"
description = "Wrapper around Playwright to help launch Camoufox"
authors = ["daijro <daijro.dev@gmail.com>"]
license = "MIT"
@ -38,6 +38,7 @@ tqdm = "*"
numpy = "*"
ua_parser = "*"
typing_extensions = "*"
screeninfo = "*"
lxml = "*"
language-tags = "*"
geoip2 = {version = "*", optional = true}