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:
icepaq
2026-03-15 21:31:49 -04:00
committed by GitHub
parent c6a6c20670
commit d6540b52ce
46 changed files with 6515 additions and 27 deletions
+1 -1
View File
@@ -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
+41 -1
View File
@@ -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)
+2
View File
@@ -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)
+37 -1
View File
@@ -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}
+2 -2
View File
@@ -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(