mirror of
https://github.com/daijro/camoufox.git
synced 2026-08-18 16:00:58 +00:00
Service Test and Contributing Guides (#521)
* example files * contributing guides * simple service test * run tests in sync * update pr template * pip updates * Update README.md * typo fixes * undo pip package update lol * upgraded service test * undo injections * test with proxies * auto set timezone and proxy url * delete checks bundle * split up service tests * split up build tests * rename service tests to service tester * Update CONTRIBUTING.md * fix entry vs exit ip * allow alpha versions * fix patch issues on macos * bidirectional patch * Add note on experimental pip package
This commit is contained in:
@@ -8,7 +8,7 @@ class CONSTRAINTS:
|
||||
The minimum and maximum supported versions of the Camoufox browser.
|
||||
"""
|
||||
|
||||
MIN_VERSION = 'beta.19'
|
||||
MIN_VERSION = 'alpha.1'
|
||||
MAX_VERSION = '1'
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import asyncio
|
||||
import json as _json
|
||||
import urllib.request
|
||||
from functools import partial
|
||||
from typing import Any, Dict, List, Optional, Union, overload
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from playwright.async_api import (
|
||||
Browser,
|
||||
@@ -101,12 +104,40 @@ async def AsyncNewBrowser(
|
||||
return await async_attach_vd(browser, virtual_display)
|
||||
|
||||
|
||||
def _proxy_url_with_creds(proxy: Dict[str, str]) -> str:
|
||||
"""Builds a proxy URL string with embedded credentials."""
|
||||
parsed = urlparse(proxy.get("server", ""))
|
||||
user = proxy.get("username", "")
|
||||
pwd = proxy.get("password", "")
|
||||
if user and pwd:
|
||||
return f"{parsed.scheme}://{user}:{pwd}@{parsed.netloc}"
|
||||
return proxy.get("server", "")
|
||||
|
||||
|
||||
async def _resolve_proxy_geo(proxy: Dict[str, str]) -> Dict[str, Optional[str]]:
|
||||
"""Queries ip-api.com through the proxy for the exit IP and timezone."""
|
||||
proxy_url = _proxy_url_with_creds(proxy)
|
||||
|
||||
def _fetch() -> Dict[str, Optional[str]]:
|
||||
handler = urllib.request.ProxyHandler({"http": proxy_url, "https": proxy_url})
|
||||
opener = urllib.request.build_opener(handler)
|
||||
try:
|
||||
with opener.open("http://ip-api.com/json?fields=query,timezone", timeout=10) as resp:
|
||||
data = _json.loads(resp.read())
|
||||
return {"ip": data.get("query") or None, "timezone": data.get("timezone") or None}
|
||||
except Exception:
|
||||
return {"ip": None, "timezone": None}
|
||||
|
||||
return await asyncio.get_event_loop().run_in_executor(None, _fetch)
|
||||
|
||||
|
||||
async def AsyncNewContext(
|
||||
browser: Browser,
|
||||
*,
|
||||
preset: Optional[Dict[str, Any]] = None,
|
||||
os: Optional[str] = None,
|
||||
ff_version: Optional[str] = None,
|
||||
webrtc_ip: Optional[str] = None,
|
||||
proxy: Optional[Dict[str, str]] = None,
|
||||
geolocation: Optional[Dict[str, float]] = None,
|
||||
**context_kwargs: Any,
|
||||
@@ -123,13 +154,22 @@ async def AsyncNewContext(
|
||||
preset: A specific fingerprint preset dict to use. If None, picks randomly.
|
||||
os: Target OS for preset selection ("windows", "macos", "linux").
|
||||
ff_version: Firefox version string for UA patching.
|
||||
webrtc_ip: IPv4 address to spoof for WebRTC ICE candidates.
|
||||
proxy: Per-context proxy (Playwright format: {"server": "...", "username": "...", "password": "..."}).
|
||||
geolocation: Per-context geolocation ({"latitude": float, "longitude": float}).
|
||||
**context_kwargs: Additional Playwright new_context() options.
|
||||
"""
|
||||
# Auto-derive WebRTC IP and timezone from proxy's exit IP when not explicitly provided
|
||||
if proxy and (not webrtc_ip or "timezone_id" not in context_kwargs):
|
||||
geo = await _resolve_proxy_geo(proxy)
|
||||
if not webrtc_ip:
|
||||
webrtc_ip = geo["ip"]
|
||||
if "timezone_id" not in context_kwargs and geo["timezone"]:
|
||||
context_kwargs["timezone_id"] = geo["timezone"]
|
||||
|
||||
fp = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: generate_context_fingerprint(preset=preset, os=os, ff_version=ff_version),
|
||||
lambda: generate_context_fingerprint(preset=preset, os=os, ff_version=ff_version, webrtc_ip=webrtc_ip),
|
||||
)
|
||||
|
||||
# Merge generated context options with user overrides (user wins)
|
||||
|
||||
@@ -419,6 +419,7 @@ def generate_context_fingerprint(
|
||||
preset: Optional[Dict] = None,
|
||||
os: Optional[str] = None,
|
||||
ff_version: Optional[str] = None,
|
||||
webrtc_ip: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate fingerprint values for a single per-context identity.
|
||||
@@ -528,6 +529,7 @@ def generate_context_fingerprint(
|
||||
'timezone': preset.get('timezone') if isinstance(preset.get('timezone'), str) else config.get('timezone'),
|
||||
'fontList': config.get('fonts'),
|
||||
'speechVoices': config.get('voices'),
|
||||
'webrtcIP': webrtc_ip or '',
|
||||
}
|
||||
|
||||
init_script = _build_init_script(init_values)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import json as _json
|
||||
import urllib.request
|
||||
from typing import Any, Dict, List, Optional, Union, overload
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from playwright.sync_api import (
|
||||
Browser,
|
||||
@@ -101,12 +104,36 @@ def NewBrowser(
|
||||
return sync_attach_vd(browser, virtual_display)
|
||||
|
||||
|
||||
def _proxy_url_with_creds(proxy: Dict[str, str]) -> str:
|
||||
"""Builds a proxy URL string with embedded credentials."""
|
||||
parsed = urlparse(proxy.get("server", ""))
|
||||
user = proxy.get("username", "")
|
||||
pwd = proxy.get("password", "")
|
||||
if user and pwd:
|
||||
return f"{parsed.scheme}://{user}:{pwd}@{parsed.netloc}"
|
||||
return proxy.get("server", "")
|
||||
|
||||
|
||||
def _resolve_proxy_geo(proxy: Dict[str, str]) -> Dict[str, Optional[str]]:
|
||||
"""Queries ip-api.com through the proxy for the exit IP and timezone."""
|
||||
proxy_url = _proxy_url_with_creds(proxy)
|
||||
handler = urllib.request.ProxyHandler({"http": proxy_url, "https": proxy_url})
|
||||
opener = urllib.request.build_opener(handler)
|
||||
try:
|
||||
with opener.open("http://ip-api.com/json?fields=query,timezone", timeout=10) as resp:
|
||||
data = _json.loads(resp.read())
|
||||
return {"ip": data.get("query") or None, "timezone": data.get("timezone") or None}
|
||||
except Exception:
|
||||
return {"ip": None, "timezone": None}
|
||||
|
||||
|
||||
def NewContext(
|
||||
browser: Browser,
|
||||
*,
|
||||
preset: Optional[Dict[str, Any]] = None,
|
||||
os: Optional[str] = None,
|
||||
ff_version: Optional[str] = None,
|
||||
webrtc_ip: Optional[str] = None,
|
||||
proxy: Optional[Dict[str, str]] = None,
|
||||
geolocation: Optional[Dict[str, float]] = None,
|
||||
**context_kwargs: Any,
|
||||
@@ -123,11 +150,20 @@ def NewContext(
|
||||
preset: A specific fingerprint preset dict to use. If None, picks randomly.
|
||||
os: Target OS for preset selection ("windows", "macos", "linux").
|
||||
ff_version: Firefox version string for UA patching.
|
||||
webrtc_ip: IPv4 address to spoof for WebRTC ICE candidates.
|
||||
proxy: Per-context proxy (Playwright format: {"server": "...", "username": "...", "password": "..."}).
|
||||
geolocation: Per-context geolocation ({"latitude": float, "longitude": float}).
|
||||
**context_kwargs: Additional Playwright new_context() options.
|
||||
"""
|
||||
fp = generate_context_fingerprint(preset=preset, os=os, ff_version=ff_version)
|
||||
# Auto-derive WebRTC IP and timezone from proxy's exit IP when not explicitly provided
|
||||
if proxy and (not webrtc_ip or "timezone_id" not in context_kwargs):
|
||||
geo = _resolve_proxy_geo(proxy)
|
||||
if not webrtc_ip:
|
||||
webrtc_ip = geo["ip"]
|
||||
if "timezone_id" not in context_kwargs and geo["timezone"]:
|
||||
context_kwargs["timezone_id"] = geo["timezone"]
|
||||
|
||||
fp = generate_context_fingerprint(preset=preset, os=os, ff_version=ff_version, webrtc_ip=webrtc_ip)
|
||||
|
||||
# Merge generated context options with user overrides (user wins)
|
||||
opts: Dict[str, Any] = {**fp['context_options'], **context_kwargs}
|
||||
|
||||
@@ -19,7 +19,6 @@ from .exceptions import (
|
||||
InvalidOS,
|
||||
InvalidPropertyType,
|
||||
NonFirefoxFingerprint,
|
||||
UnknownProperty,
|
||||
)
|
||||
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
|
||||
@@ -113,7 +112,8 @@ def validate_config(config_map: Dict[str, str], path: Optional[Path] = None) ->
|
||||
for key, value in config_map.items():
|
||||
expected_type = property_types.get(key)
|
||||
if not expected_type:
|
||||
raise UnknownProperty(f"Unknown property {key} in config")
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user