mirror of
https://forge.fsky.io/oneflux/omegafox.git
synced 2026-02-10 16:32:05 -08:00
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:
parent
b371928d43
commit
ad55622550
11 changed files with 100 additions and 48 deletions
|
|
@ -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]]):
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]]):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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]]):
|
||||||
|
|
|
||||||
|
|
@ -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 {}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
11
pythonlib/publish.sh
Normal 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
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue