2026-02-22 09:01:10 +01:00
|
|
|
"""Core browser launch functions for cloakbrowser.
|
|
|
|
|
|
|
|
|
|
Provides launch() and launch_async() — thin wrappers around Playwright
|
|
|
|
|
that use our patched stealth Chromium binary instead of stock Chromium.
|
|
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
from cloakbrowser import launch
|
|
|
|
|
|
|
|
|
|
browser = launch()
|
|
|
|
|
page = browser.new_page()
|
|
|
|
|
page.goto("https://protected-site.com")
|
|
|
|
|
browser.close()
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import logging
|
2026-03-04 12:19:56 +03:00
|
|
|
import os
|
2026-03-04 20:11:30 +01:00
|
|
|
from typing import Any, Literal, TypedDict
|
2026-02-24 19:00:12 +01:00
|
|
|
from urllib.parse import unquote, urlparse, urlunparse
|
2026-02-22 09:01:10 +01:00
|
|
|
|
2026-03-10 20:22:10 +01:00
|
|
|
from .config import DEFAULT_VIEWPORT, IGNORE_DEFAULT_ARGS, get_default_stealth_args
|
2026-02-22 09:01:10 +01:00
|
|
|
from .download import ensure_binary
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("cloakbrowser")
|
|
|
|
|
|
|
|
|
|
|
2026-03-10 03:29:38 +01:00
|
|
|
def _resolve_timezone(timezone: str | None, kwargs: dict[str, Any]) -> str | None:
|
|
|
|
|
"""Accept both timezone and timezone_id — either works, no warning."""
|
2026-03-05 02:13:46 +01:00
|
|
|
if "timezone_id" in kwargs:
|
|
|
|
|
if timezone is None:
|
|
|
|
|
timezone = kwargs.pop("timezone_id")
|
|
|
|
|
else:
|
|
|
|
|
kwargs.pop("timezone_id")
|
|
|
|
|
return timezone
|
|
|
|
|
|
|
|
|
|
|
2026-03-04 20:11:30 +01:00
|
|
|
class _ProxySettingsRequired(TypedDict):
|
|
|
|
|
server: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ProxySettings(_ProxySettingsRequired, total=False):
|
|
|
|
|
"""Playwright-compatible proxy configuration."""
|
|
|
|
|
|
|
|
|
|
bypass: str
|
|
|
|
|
username: str
|
|
|
|
|
password: str
|
|
|
|
|
|
|
|
|
|
|
2026-02-22 09:01:10 +01:00
|
|
|
def launch(
|
|
|
|
|
headless: bool = True,
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy: str | ProxySettings | None = None,
|
2026-02-22 09:01:10 +01:00
|
|
|
args: list[str] | None = None,
|
|
|
|
|
stealth_args: bool = True,
|
2026-03-01 01:07:11 +01:00
|
|
|
timezone: str | None = None,
|
|
|
|
|
locale: str | None = None,
|
|
|
|
|
geoip: bool = False,
|
2026-03-05 18:33:47 +01:00
|
|
|
backend: str | None = None,
|
2026-03-08 12:49:42 +03:00
|
|
|
humanize: bool = False,
|
|
|
|
|
human_preset: str = "default",
|
|
|
|
|
human_config: dict | None = None,
|
2026-02-22 09:01:10 +01:00
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> Any:
|
|
|
|
|
"""Launch stealth Chromium browser. Returns a Playwright Browser object.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
headless: Run in headless mode (default True).
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy: Proxy URL string or Playwright proxy dict.
|
|
|
|
|
String: 'http://user:pass@proxy:8080' (credentials auto-extracted).
|
|
|
|
|
Dict: {"server": "http://proxy:8080", "bypass": ".google.com", ...}
|
|
|
|
|
— passed directly to Playwright.
|
2026-02-22 09:01:10 +01:00
|
|
|
args: Additional Chromium CLI arguments to pass.
|
|
|
|
|
stealth_args: Include default stealth fingerprint args (default True).
|
|
|
|
|
Set to False if you want to pass your own --fingerprint flags.
|
2026-03-02 18:20:57 +01:00
|
|
|
timezone: IANA timezone (e.g. 'America/New_York'). Sets --fingerprint-timezone binary flag.
|
2026-03-01 01:07:11 +01:00
|
|
|
locale: BCP 47 locale (e.g. 'en-US'). Sets --lang binary flag.
|
|
|
|
|
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
|
|
|
|
Requires ``pip install cloakbrowser[geoip]``. Downloads ~70 MB
|
|
|
|
|
GeoLite2-City database on first use. Explicit timezone/locale
|
|
|
|
|
always override geoip results.
|
2026-03-05 18:33:47 +01:00
|
|
|
backend: Playwright backend — 'playwright' (default) or 'patchright'.
|
|
|
|
|
Patchright suppresses CDP signals (helps reCAPTCHA v3 Enterprise)
|
|
|
|
|
but breaks proxy auth and add_init_script.
|
|
|
|
|
Override globally with CLOAKBROWSER_BACKEND env var.
|
2026-03-08 12:49:42 +03:00
|
|
|
humanize: Enable human-like mouse, keyboard, scroll behavior (default False).
|
|
|
|
|
human_preset: Humanize preset — 'default' or 'careful' (default 'default').
|
|
|
|
|
human_config: Custom humanize config dict to override preset values.
|
2026-02-22 09:01:10 +01:00
|
|
|
**kwargs: Passed directly to playwright.chromium.launch().
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Playwright Browser object — use same API as playwright.chromium.launch().
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> from cloakbrowser import launch
|
|
|
|
|
>>> browser = launch()
|
|
|
|
|
>>> page = browser.new_page()
|
|
|
|
|
>>> page.goto("https://bot.incolumitas.com")
|
|
|
|
|
>>> print(page.title())
|
|
|
|
|
>>> browser.close()
|
|
|
|
|
"""
|
2026-03-05 18:33:47 +01:00
|
|
|
sync_playwright = _import_sync_playwright(_resolve_backend(backend))
|
2026-02-22 09:01:10 +01:00
|
|
|
|
|
|
|
|
binary_path = ensure_binary()
|
2026-04-02 18:34:17 +02:00
|
|
|
timezone, locale = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
|
|
|
|
chrome_args = build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
2026-02-22 09:01:10 +01:00
|
|
|
|
|
|
|
|
logger.debug("Launching stealth Chromium (headless=%s, args=%d)", headless, len(chrome_args))
|
|
|
|
|
|
|
|
|
|
pw = sync_playwright().start()
|
|
|
|
|
browser = pw.chromium.launch(
|
|
|
|
|
executable_path=binary_path,
|
|
|
|
|
headless=headless,
|
|
|
|
|
args=chrome_args,
|
2026-03-10 20:22:10 +01:00
|
|
|
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
2026-02-22 09:01:10 +01:00
|
|
|
**_build_proxy_kwargs(proxy),
|
|
|
|
|
**kwargs,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Patch close() to also stop the Playwright instance
|
|
|
|
|
_original_close = browser.close
|
|
|
|
|
|
|
|
|
|
def _close_with_cleanup() -> None:
|
2026-03-15 17:51:03 +01:00
|
|
|
try:
|
|
|
|
|
_original_close()
|
|
|
|
|
finally:
|
|
|
|
|
pw.stop()
|
2026-02-22 09:01:10 +01:00
|
|
|
|
|
|
|
|
browser.close = _close_with_cleanup
|
|
|
|
|
|
2026-03-08 12:49:42 +03:00
|
|
|
# Human-like behavioral patching
|
|
|
|
|
if humanize:
|
|
|
|
|
from .human import patch_browser
|
|
|
|
|
from .human.config import resolve_config
|
|
|
|
|
cfg = resolve_config(human_preset, human_config)
|
|
|
|
|
patch_browser(browser, cfg)
|
|
|
|
|
|
2026-02-22 09:01:10 +01:00
|
|
|
return browser
|
|
|
|
|
|
|
|
|
|
|
2026-03-08 12:49:42 +03:00
|
|
|
async def launch_async( # noqa: C901
|
2026-02-22 09:01:10 +01:00
|
|
|
headless: bool = True,
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy: str | ProxySettings | None = None,
|
2026-02-22 09:01:10 +01:00
|
|
|
args: list[str] | None = None,
|
|
|
|
|
stealth_args: bool = True,
|
2026-03-01 01:07:11 +01:00
|
|
|
timezone: str | None = None,
|
|
|
|
|
locale: str | None = None,
|
|
|
|
|
geoip: bool = False,
|
2026-03-05 18:33:47 +01:00
|
|
|
backend: str | None = None,
|
2026-03-08 12:49:42 +03:00
|
|
|
humanize: bool = False,
|
|
|
|
|
human_preset: str = "default",
|
|
|
|
|
human_config: dict | None = None,
|
2026-02-22 09:01:10 +01:00
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> Any:
|
|
|
|
|
"""Async version of launch(). Returns a Playwright Browser object.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
headless: Run in headless mode (default True).
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
2026-02-22 09:01:10 +01:00
|
|
|
args: Additional Chromium CLI arguments to pass.
|
|
|
|
|
stealth_args: Include default stealth fingerprint args (default True).
|
2026-03-02 18:20:57 +01:00
|
|
|
timezone: IANA timezone (e.g. 'America/New_York'). Sets --fingerprint-timezone binary flag.
|
2026-03-01 01:07:11 +01:00
|
|
|
locale: BCP 47 locale (e.g. 'en-US'). Sets --lang binary flag.
|
|
|
|
|
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
2026-03-05 18:33:47 +01:00
|
|
|
backend: Playwright backend — 'playwright' (default) or 'patchright'.
|
2026-03-08 12:49:42 +03:00
|
|
|
humanize: Enable human-like mouse, keyboard, scroll behavior (default False).
|
|
|
|
|
human_preset: Humanize preset — 'default' or 'careful' (default 'default').
|
|
|
|
|
human_config: Custom humanize config dict to override preset values.
|
2026-02-22 09:01:10 +01:00
|
|
|
**kwargs: Passed directly to playwright.chromium.launch().
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Playwright Browser object (async API).
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> import asyncio
|
|
|
|
|
>>> from cloakbrowser import launch_async
|
|
|
|
|
>>>
|
|
|
|
|
>>> async def main():
|
|
|
|
|
... browser = await launch_async()
|
|
|
|
|
... page = await browser.new_page()
|
|
|
|
|
... await page.goto("https://bot.incolumitas.com")
|
|
|
|
|
... print(await page.title())
|
|
|
|
|
... await browser.close()
|
|
|
|
|
>>>
|
|
|
|
|
>>> asyncio.run(main())
|
|
|
|
|
"""
|
2026-03-05 18:33:47 +01:00
|
|
|
async_playwright = _import_async_playwright(_resolve_backend(backend))
|
2026-02-22 09:01:10 +01:00
|
|
|
|
|
|
|
|
binary_path = ensure_binary()
|
2026-04-02 18:34:17 +02:00
|
|
|
timezone, locale = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
|
|
|
|
chrome_args = build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
2026-02-22 09:01:10 +01:00
|
|
|
|
|
|
|
|
logger.debug("Launching stealth Chromium async (headless=%s, args=%d)", headless, len(chrome_args))
|
|
|
|
|
|
|
|
|
|
pw = await async_playwright().start()
|
|
|
|
|
browser = await pw.chromium.launch(
|
|
|
|
|
executable_path=binary_path,
|
|
|
|
|
headless=headless,
|
|
|
|
|
args=chrome_args,
|
2026-03-10 20:22:10 +01:00
|
|
|
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
2026-02-22 09:01:10 +01:00
|
|
|
**_build_proxy_kwargs(proxy),
|
|
|
|
|
**kwargs,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Patch close() to also stop the Playwright instance
|
|
|
|
|
_original_close = browser.close
|
|
|
|
|
|
|
|
|
|
async def _close_with_cleanup() -> None:
|
2026-03-15 17:51:03 +01:00
|
|
|
try:
|
|
|
|
|
await _original_close()
|
|
|
|
|
finally:
|
|
|
|
|
await pw.stop()
|
2026-02-22 09:01:10 +01:00
|
|
|
|
|
|
|
|
browser.close = _close_with_cleanup
|
|
|
|
|
|
2026-03-08 12:49:42 +03:00
|
|
|
# Human-like behavioral patching (async variant)
|
|
|
|
|
if humanize:
|
|
|
|
|
from .human import patch_browser_async
|
|
|
|
|
from .human.config import resolve_config
|
|
|
|
|
cfg = resolve_config(human_preset, human_config)
|
|
|
|
|
patch_browser_async(browser, cfg)
|
|
|
|
|
|
2026-02-22 09:01:10 +01:00
|
|
|
return browser
|
|
|
|
|
|
|
|
|
|
|
2026-03-04 12:19:56 +03:00
|
|
|
def launch_persistent_context(
|
|
|
|
|
user_data_dir: str | os.PathLike,
|
|
|
|
|
headless: bool = True,
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy: str | ProxySettings | None = None,
|
2026-03-04 12:19:56 +03:00
|
|
|
args: list[str] | None = None,
|
|
|
|
|
stealth_args: bool = True,
|
|
|
|
|
user_agent: str | None = None,
|
|
|
|
|
viewport: dict | None = None,
|
|
|
|
|
locale: str | None = None,
|
2026-03-05 02:13:46 +01:00
|
|
|
timezone: str | None = None,
|
2026-03-04 12:19:56 +03:00
|
|
|
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
|
|
|
|
|
geoip: bool = False,
|
2026-03-05 18:33:47 +01:00
|
|
|
backend: str | None = None,
|
2026-03-08 12:49:42 +03:00
|
|
|
humanize: bool = False,
|
|
|
|
|
human_preset: str = "default",
|
|
|
|
|
human_config: dict | None = None,
|
2026-03-04 12:19:56 +03:00
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> Any:
|
|
|
|
|
"""Launch stealth browser with a persistent profile and return a BrowserContext.
|
|
|
|
|
|
|
|
|
|
This persists cookies, localStorage, cache, and other browser state across
|
|
|
|
|
sessions by storing them in ``user_data_dir``. Also avoids incognito detection
|
|
|
|
|
by services like BrowserScan (-10% penalty).
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
user_data_dir: Path to the directory where browser profile data is stored.
|
|
|
|
|
Created automatically if it doesn't exist. Reuse the same path across
|
|
|
|
|
sessions to restore cookies, localStorage, cached credentials, etc.
|
|
|
|
|
headless: Run in headless mode (default True).
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
2026-03-04 12:19:56 +03:00
|
|
|
args: Additional Chromium CLI arguments.
|
|
|
|
|
stealth_args: Include default stealth fingerprint args (default True).
|
|
|
|
|
user_agent: Custom user agent string.
|
|
|
|
|
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
|
|
|
|
locale: Browser locale, e.g. "en-US".
|
2026-03-05 02:13:46 +01:00
|
|
|
timezone: IANA timezone (e.g. 'America/New_York').
|
2026-03-04 12:19:56 +03:00
|
|
|
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
|
|
|
|
|
Default: None (uses Chromium default, which is 'light').
|
|
|
|
|
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
|
|
|
|
Requires ``pip install cloakbrowser[geoip]``.
|
2026-03-05 18:33:47 +01:00
|
|
|
backend: Playwright backend — 'playwright' (default) or 'patchright'.
|
2026-03-08 12:49:42 +03:00
|
|
|
humanize: Enable human-like mouse, keyboard, scroll behavior (default False).
|
|
|
|
|
human_preset: Humanize preset — 'default' or 'careful' (default 'default').
|
|
|
|
|
human_config: Custom humanize config dict to override preset values.
|
2026-03-04 12:19:56 +03:00
|
|
|
**kwargs: Passed directly to playwright.chromium.launch_persistent_context().
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Playwright BrowserContext object backed by a persistent profile.
|
|
|
|
|
Call ``.close()`` when done — this also stops the Playwright instance.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> from cloakbrowser import launch_persistent_context
|
|
|
|
|
>>> ctx = launch_persistent_context("./my-profile", headless=False)
|
|
|
|
|
>>> page = ctx.new_page()
|
|
|
|
|
>>> page.goto("https://protected-site.com")
|
|
|
|
|
>>> ctx.close() # Profile is saved; re-use path next run to restore state.
|
|
|
|
|
"""
|
2026-03-05 18:33:47 +01:00
|
|
|
sync_playwright = _import_sync_playwright(_resolve_backend(backend))
|
2026-03-04 12:19:56 +03:00
|
|
|
|
2026-03-10 03:29:38 +01:00
|
|
|
timezone = _resolve_timezone(timezone, kwargs)
|
2026-03-05 02:13:46 +01:00
|
|
|
|
2026-03-04 12:19:56 +03:00
|
|
|
binary_path = ensure_binary()
|
2026-04-02 18:34:17 +02:00
|
|
|
timezone, locale = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
|
|
|
|
chrome_args = build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
2026-03-04 12:19:56 +03:00
|
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
|
|
|
|
|
headless,
|
|
|
|
|
user_data_dir,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-10 03:29:38 +01:00
|
|
|
# locale and timezone are set via binary flags (--lang, --fingerprint-timezone)
|
|
|
|
|
# — NOT via Playwright context kwargs which use detectable CDP emulation.
|
2026-03-04 12:19:56 +03:00
|
|
|
context_kwargs: dict[str, Any] = {}
|
|
|
|
|
if user_agent:
|
|
|
|
|
context_kwargs["user_agent"] = user_agent
|
|
|
|
|
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
|
|
|
|
|
if color_scheme:
|
|
|
|
|
context_kwargs["color_scheme"] = color_scheme
|
|
|
|
|
context_kwargs.update(kwargs)
|
|
|
|
|
|
|
|
|
|
pw = sync_playwright().start()
|
|
|
|
|
context = pw.chromium.launch_persistent_context(
|
|
|
|
|
user_data_dir=os.fspath(user_data_dir),
|
|
|
|
|
executable_path=binary_path,
|
|
|
|
|
headless=headless,
|
|
|
|
|
args=chrome_args,
|
2026-03-10 20:22:10 +01:00
|
|
|
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
2026-03-04 12:19:56 +03:00
|
|
|
**_build_proxy_kwargs(proxy),
|
|
|
|
|
**context_kwargs,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Patch close() to also stop the Playwright instance
|
|
|
|
|
_original_close = context.close
|
|
|
|
|
|
|
|
|
|
def _close_with_cleanup() -> None:
|
2026-03-15 17:51:03 +01:00
|
|
|
try:
|
|
|
|
|
_original_close()
|
|
|
|
|
finally:
|
|
|
|
|
pw.stop()
|
2026-03-04 12:19:56 +03:00
|
|
|
|
|
|
|
|
context.close = _close_with_cleanup
|
|
|
|
|
|
2026-03-08 12:49:42 +03:00
|
|
|
# Human-like behavioral patching
|
|
|
|
|
if humanize:
|
|
|
|
|
from .human import patch_context
|
|
|
|
|
from .human.config import resolve_config
|
|
|
|
|
cfg = resolve_config(human_preset, human_config)
|
|
|
|
|
patch_context(context, cfg)
|
|
|
|
|
|
2026-03-04 12:19:56 +03:00
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def launch_persistent_context_async(
|
|
|
|
|
user_data_dir: str | os.PathLike,
|
|
|
|
|
headless: bool = True,
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy: str | ProxySettings | None = None,
|
2026-03-04 12:19:56 +03:00
|
|
|
args: list[str] | None = None,
|
|
|
|
|
stealth_args: bool = True,
|
|
|
|
|
user_agent: str | None = None,
|
|
|
|
|
viewport: dict | None = None,
|
|
|
|
|
locale: str | None = None,
|
2026-03-05 02:13:46 +01:00
|
|
|
timezone: str | None = None,
|
2026-03-04 12:19:56 +03:00
|
|
|
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
|
|
|
|
|
geoip: bool = False,
|
2026-03-05 18:33:47 +01:00
|
|
|
backend: str | None = None,
|
2026-03-08 12:49:42 +03:00
|
|
|
humanize: bool = False,
|
|
|
|
|
human_preset: str = "default",
|
|
|
|
|
human_config: dict | None = None,
|
2026-03-04 12:19:56 +03:00
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> Any:
|
|
|
|
|
"""Async version of launch_persistent_context().
|
|
|
|
|
|
|
|
|
|
Launch stealth browser with a persistent profile and return a BrowserContext.
|
|
|
|
|
This persists cookies, localStorage, cache, and other browser state across
|
|
|
|
|
sessions by storing them in ``user_data_dir``.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
user_data_dir: Path to the directory where browser profile data is stored.
|
|
|
|
|
Created automatically if it doesn't exist.
|
|
|
|
|
headless: Run in headless mode (default True).
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
2026-03-04 12:19:56 +03:00
|
|
|
args: Additional Chromium CLI arguments.
|
|
|
|
|
stealth_args: Include default stealth fingerprint args (default True).
|
|
|
|
|
user_agent: Custom user agent string.
|
|
|
|
|
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
|
|
|
|
locale: Browser locale, e.g. "en-US".
|
2026-03-05 02:13:46 +01:00
|
|
|
timezone: IANA timezone (e.g. 'America/New_York').
|
2026-03-04 12:19:56 +03:00
|
|
|
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
|
|
|
|
|
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
2026-03-05 18:33:47 +01:00
|
|
|
backend: Playwright backend — 'playwright' (default) or 'patchright'.
|
2026-03-08 12:49:42 +03:00
|
|
|
humanize: Enable human-like mouse, keyboard, scroll behavior (default False).
|
|
|
|
|
human_preset: Humanize preset — 'default' or 'careful' (default 'default').
|
|
|
|
|
human_config: Custom humanize config dict to override preset values.
|
2026-03-04 12:19:56 +03:00
|
|
|
**kwargs: Passed directly to playwright.chromium.launch_persistent_context().
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Playwright BrowserContext object backed by a persistent profile (async API).
|
|
|
|
|
Call ``await .close()`` when done.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> import asyncio
|
|
|
|
|
>>> from cloakbrowser import launch_persistent_context_async
|
|
|
|
|
>>>
|
|
|
|
|
>>> async def main():
|
|
|
|
|
... ctx = await launch_persistent_context_async("./my-profile", headless=False)
|
|
|
|
|
... page = await ctx.new_page()
|
|
|
|
|
... await page.goto("https://protected-site.com")
|
|
|
|
|
... await ctx.close()
|
|
|
|
|
>>>
|
|
|
|
|
>>> asyncio.run(main())
|
|
|
|
|
"""
|
2026-03-05 18:33:47 +01:00
|
|
|
async_playwright = _import_async_playwright(_resolve_backend(backend))
|
2026-03-04 12:19:56 +03:00
|
|
|
|
2026-03-10 03:29:38 +01:00
|
|
|
timezone = _resolve_timezone(timezone, kwargs)
|
2026-03-05 02:13:46 +01:00
|
|
|
|
2026-03-04 12:19:56 +03:00
|
|
|
binary_path = ensure_binary()
|
2026-04-02 18:34:17 +02:00
|
|
|
timezone, locale = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
|
|
|
|
chrome_args = build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
2026-03-04 12:19:56 +03:00
|
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
|
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
|
|
|
|
|
headless,
|
|
|
|
|
user_data_dir,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-10 03:29:38 +01:00
|
|
|
# locale and timezone are set via binary flags (--lang, --fingerprint-timezone)
|
|
|
|
|
# — NOT via Playwright context kwargs which use detectable CDP emulation.
|
2026-03-04 12:19:56 +03:00
|
|
|
context_kwargs: dict[str, Any] = {}
|
|
|
|
|
if user_agent:
|
|
|
|
|
context_kwargs["user_agent"] = user_agent
|
|
|
|
|
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
|
|
|
|
|
if color_scheme:
|
|
|
|
|
context_kwargs["color_scheme"] = color_scheme
|
|
|
|
|
context_kwargs.update(kwargs)
|
|
|
|
|
|
|
|
|
|
pw = await async_playwright().start()
|
|
|
|
|
context = await pw.chromium.launch_persistent_context(
|
|
|
|
|
user_data_dir=os.fspath(user_data_dir),
|
|
|
|
|
executable_path=binary_path,
|
|
|
|
|
headless=headless,
|
|
|
|
|
args=chrome_args,
|
2026-03-10 20:22:10 +01:00
|
|
|
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
2026-03-04 12:19:56 +03:00
|
|
|
**_build_proxy_kwargs(proxy),
|
|
|
|
|
**context_kwargs,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Patch close() to also stop the Playwright instance
|
|
|
|
|
_original_close = context.close
|
|
|
|
|
|
|
|
|
|
async def _close_with_cleanup() -> None:
|
2026-03-15 17:51:03 +01:00
|
|
|
try:
|
|
|
|
|
await _original_close()
|
|
|
|
|
finally:
|
|
|
|
|
await pw.stop()
|
2026-03-04 12:19:56 +03:00
|
|
|
|
|
|
|
|
context.close = _close_with_cleanup
|
|
|
|
|
|
2026-03-08 12:49:42 +03:00
|
|
|
# Human-like behavioral patching (async variant)
|
|
|
|
|
if humanize:
|
|
|
|
|
from .human import patch_context_async
|
|
|
|
|
from .human.config import resolve_config
|
|
|
|
|
cfg = resolve_config(human_preset, human_config)
|
|
|
|
|
patch_context_async(context, cfg)
|
|
|
|
|
|
2026-03-04 12:19:56 +03:00
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
2026-02-22 09:01:10 +01:00
|
|
|
def launch_context(
|
|
|
|
|
headless: bool = True,
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy: str | ProxySettings | None = None,
|
2026-02-22 09:01:10 +01:00
|
|
|
args: list[str] | None = None,
|
|
|
|
|
stealth_args: bool = True,
|
|
|
|
|
user_agent: str | None = None,
|
|
|
|
|
viewport: dict | None = None,
|
|
|
|
|
locale: str | None = None,
|
2026-03-05 02:13:46 +01:00
|
|
|
timezone: str | None = None,
|
2026-03-01 02:21:53 +01:00
|
|
|
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
|
2026-03-01 01:07:11 +01:00
|
|
|
geoip: bool = False,
|
2026-03-05 18:33:47 +01:00
|
|
|
backend: str | None = None,
|
2026-03-08 12:49:42 +03:00
|
|
|
humanize: bool = False,
|
|
|
|
|
human_preset: str = "default",
|
|
|
|
|
human_config: dict | None = None,
|
2026-02-22 09:01:10 +01:00
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> Any:
|
|
|
|
|
"""Launch stealth browser and return a BrowserContext with common options pre-set.
|
|
|
|
|
|
|
|
|
|
Convenience function that creates a browser + context in one call.
|
|
|
|
|
Useful for setting user agent, viewport, locale, etc.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
headless: Run in headless mode (default True).
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
2026-02-22 09:01:10 +01:00
|
|
|
args: Additional Chromium CLI arguments.
|
|
|
|
|
stealth_args: Include default stealth fingerprint args (default True).
|
|
|
|
|
user_agent: Custom user agent string.
|
|
|
|
|
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
|
|
|
|
locale: Browser locale, e.g. "en-US".
|
2026-03-05 02:13:46 +01:00
|
|
|
timezone: IANA timezone (e.g. 'America/New_York').
|
2026-02-27 03:56:54 +01:00
|
|
|
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
|
|
|
|
|
Default: None (uses Chromium default, which is 'light').
|
2026-03-01 01:07:11 +01:00
|
|
|
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
2026-03-05 18:33:47 +01:00
|
|
|
backend: Playwright backend — 'playwright' (default) or 'patchright'.
|
2026-03-08 12:49:42 +03:00
|
|
|
humanize: Enable human-like mouse, keyboard, scroll behavior (default False).
|
|
|
|
|
human_preset: Humanize preset — 'default' or 'careful' (default 'default').
|
|
|
|
|
human_config: Custom humanize config dict to override preset values.
|
2026-02-22 09:01:10 +01:00
|
|
|
**kwargs: Passed to browser.new_context().
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Playwright BrowserContext object.
|
|
|
|
|
"""
|
2026-03-10 03:29:38 +01:00
|
|
|
timezone = _resolve_timezone(timezone, kwargs)
|
2026-03-05 02:13:46 +01:00
|
|
|
|
2026-03-01 01:07:11 +01:00
|
|
|
# Resolve geoip BEFORE launch() to avoid double-resolution and ensure
|
2026-03-10 03:29:38 +01:00
|
|
|
# resolved values flow to binary flags
|
2026-04-02 18:34:17 +02:00
|
|
|
timezone, locale = maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
2026-03-10 03:29:38 +01:00
|
|
|
# --fingerprint-timezone is process-wide (reads CommandLine in renderer),
|
|
|
|
|
# so it applies to ALL contexts, not just the default one.
|
|
|
|
|
# locale and timezone are set via binary flags only — no CDP emulation.
|
2026-03-01 01:07:11 +01:00
|
|
|
browser = launch(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args,
|
2026-03-10 03:29:38 +01:00
|
|
|
timezone=timezone, locale=locale, backend=backend)
|
2026-02-22 09:01:10 +01:00
|
|
|
|
|
|
|
|
context_kwargs: dict[str, Any] = {}
|
|
|
|
|
if user_agent:
|
|
|
|
|
context_kwargs["user_agent"] = user_agent
|
2026-02-27 03:56:54 +01:00
|
|
|
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
|
|
|
|
|
if color_scheme:
|
|
|
|
|
context_kwargs["color_scheme"] = color_scheme
|
2026-02-22 09:01:10 +01:00
|
|
|
context_kwargs.update(kwargs)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
context = browser.new_context(**context_kwargs)
|
|
|
|
|
except Exception:
|
|
|
|
|
browser.close()
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
# Patch close() to also close the browser (and its Playwright instance)
|
|
|
|
|
_original_ctx_close = context.close
|
|
|
|
|
|
|
|
|
|
def _close_context_with_cleanup() -> None:
|
2026-03-15 17:51:03 +01:00
|
|
|
try:
|
|
|
|
|
_original_ctx_close()
|
|
|
|
|
finally:
|
|
|
|
|
browser.close()
|
2026-02-22 09:01:10 +01:00
|
|
|
|
|
|
|
|
context.close = _close_context_with_cleanup
|
|
|
|
|
|
2026-03-08 12:49:42 +03:00
|
|
|
# Human-like behavioral patching
|
|
|
|
|
if humanize:
|
|
|
|
|
from .human import patch_context
|
|
|
|
|
from .human.config import resolve_config
|
|
|
|
|
cfg = resolve_config(human_preset, human_config)
|
|
|
|
|
patch_context(context, cfg)
|
|
|
|
|
|
2026-02-22 09:01:10 +01:00
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
2026-03-05 18:33:47 +01:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Backend resolution
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_backend(backend: str | None) -> str:
|
|
|
|
|
"""Resolve backend: param > env var > default ('playwright')."""
|
|
|
|
|
b = backend or os.environ.get("CLOAKBROWSER_BACKEND", "playwright")
|
|
|
|
|
if b not in ("playwright", "patchright"):
|
|
|
|
|
raise ValueError(f"Unknown backend '{b}'. Use 'playwright' or 'patchright'.")
|
|
|
|
|
return b
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _import_sync_playwright(backend: str):
|
|
|
|
|
"""Import sync_playwright from the resolved backend."""
|
|
|
|
|
if backend == "patchright":
|
|
|
|
|
try:
|
|
|
|
|
from patchright.sync_api import sync_playwright
|
|
|
|
|
except ModuleNotFoundError:
|
|
|
|
|
raise ModuleNotFoundError(
|
|
|
|
|
"patchright is not installed. Install it with: pip install cloakbrowser[patchright]"
|
|
|
|
|
) from None
|
|
|
|
|
return sync_playwright
|
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
|
return sync_playwright
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _import_async_playwright(backend: str):
|
|
|
|
|
"""Import async_playwright from the resolved backend."""
|
|
|
|
|
if backend == "patchright":
|
|
|
|
|
try:
|
|
|
|
|
from patchright.async_api import async_playwright
|
|
|
|
|
except ModuleNotFoundError:
|
|
|
|
|
raise ModuleNotFoundError(
|
|
|
|
|
"patchright is not installed. Install it with: pip install cloakbrowser[patchright]"
|
|
|
|
|
) from None
|
|
|
|
|
return async_playwright
|
|
|
|
|
from playwright.async_api import async_playwright
|
|
|
|
|
return async_playwright
|
|
|
|
|
|
|
|
|
|
|
2026-02-22 09:01:10 +01:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Internal helpers
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
2026-03-09 04:12:54 +01:00
|
|
|
def _ensure_proxy_scheme(proxy_url: str) -> str:
|
|
|
|
|
"""Prepend http:// to schemeless proxy URLs so parsers can extract hostname."""
|
|
|
|
|
return proxy_url if "://" in proxy_url else f"http://{proxy_url}"
|
|
|
|
|
|
|
|
|
|
|
2026-04-02 18:34:17 +02:00
|
|
|
def maybe_resolve_geoip(
|
2026-03-01 01:07:11 +01:00
|
|
|
geoip: bool,
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy: str | ProxySettings | None,
|
2026-03-01 01:07:11 +01:00
|
|
|
timezone: str | None,
|
|
|
|
|
locale: str | None,
|
|
|
|
|
) -> tuple[str | None, str | None]:
|
|
|
|
|
"""Auto-fill timezone/locale from proxy IP when geoip is enabled."""
|
|
|
|
|
if not geoip or not proxy or (timezone is not None and locale is not None):
|
|
|
|
|
return timezone, locale
|
|
|
|
|
|
|
|
|
|
from .geoip import resolve_proxy_geo
|
|
|
|
|
|
2026-03-04 20:11:30 +01:00
|
|
|
proxy_url = proxy.get("server") if isinstance(proxy, dict) else proxy
|
|
|
|
|
if not proxy_url:
|
|
|
|
|
return timezone, locale
|
2026-03-09 04:12:54 +01:00
|
|
|
proxy_url = _ensure_proxy_scheme(proxy_url)
|
2026-03-04 20:11:30 +01:00
|
|
|
geo_tz, geo_locale = resolve_proxy_geo(proxy_url)
|
2026-03-01 01:07:11 +01:00
|
|
|
if timezone is None:
|
|
|
|
|
timezone = geo_tz
|
|
|
|
|
if locale is None:
|
|
|
|
|
locale = geo_locale
|
|
|
|
|
return timezone, locale
|
|
|
|
|
|
|
|
|
|
|
2026-04-02 18:34:17 +02:00
|
|
|
def build_args(
|
2026-03-01 01:07:11 +01:00
|
|
|
stealth_args: bool,
|
|
|
|
|
extra_args: list[str] | None,
|
|
|
|
|
timezone: str | None = None,
|
|
|
|
|
locale: str | None = None,
|
2026-03-15 01:18:32 +01:00
|
|
|
headless: bool = True,
|
2026-03-01 01:07:11 +01:00
|
|
|
) -> list[str]:
|
2026-03-05 07:03:57 +01:00
|
|
|
"""Combine stealth args with user-provided args and locale flags.
|
|
|
|
|
|
|
|
|
|
Deduplicates by flag key (everything before '=').
|
|
|
|
|
Priority: stealth defaults < user args < dedicated params (timezone/locale).
|
|
|
|
|
"""
|
|
|
|
|
seen: dict[str, str] = {}
|
|
|
|
|
|
2026-02-22 09:01:10 +01:00
|
|
|
if stealth_args:
|
2026-03-05 07:03:57 +01:00
|
|
|
for arg in get_default_stealth_args():
|
|
|
|
|
seen[arg.split("=", 1)[0]] = arg
|
|
|
|
|
|
2026-03-15 01:18:32 +01:00
|
|
|
# GPU blocklist bypass:
|
|
|
|
|
# - Headed mode (all platforms): Chromium blocks WebGL on software GPUs
|
|
|
|
|
# in Docker/Xvfb. Flag lets SwiftShader serve WebGL. See issue #56.
|
|
|
|
|
# - Windows (all modes): Chromium's GPU blocklist blocks WebGPU for the
|
|
|
|
|
# Microsoft Basic Render Driver. Dawn's adapter_blocklist bypass alone
|
|
|
|
|
# isn't enough — need this flag too. Linux doesn't need it.
|
|
|
|
|
import platform as _platform
|
|
|
|
|
if not headless or _platform.system() == "Windows":
|
|
|
|
|
seen["--ignore-gpu-blocklist"] = "--ignore-gpu-blocklist"
|
|
|
|
|
|
2026-02-22 09:01:10 +01:00
|
|
|
if extra_args:
|
2026-03-05 07:03:57 +01:00
|
|
|
for arg in extra_args:
|
|
|
|
|
key = arg.split("=", 1)[0]
|
|
|
|
|
if key in seen:
|
|
|
|
|
logger.debug("Arg override: %s -> %s", seen[key], arg)
|
|
|
|
|
seen[key] = arg
|
|
|
|
|
|
2026-03-01 01:07:11 +01:00
|
|
|
# Timezone/locale flags are independent of stealth_args — always inject when set
|
|
|
|
|
if timezone:
|
2026-03-05 07:03:57 +01:00
|
|
|
key = "--fingerprint-timezone"
|
|
|
|
|
flag = f"{key}={timezone}"
|
|
|
|
|
if key in seen:
|
|
|
|
|
logger.debug("Arg override: %s -> %s", seen[key], flag)
|
|
|
|
|
seen[key] = flag
|
2026-03-01 01:07:11 +01:00
|
|
|
if locale:
|
2026-03-10 03:29:38 +01:00
|
|
|
for key in ("--lang", "--fingerprint-locale"):
|
|
|
|
|
flag = f"{key}={locale}"
|
|
|
|
|
if key in seen:
|
|
|
|
|
logger.debug("Arg override: %s -> %s", seen[key], flag)
|
|
|
|
|
seen[key] = flag
|
2026-03-05 07:03:57 +01:00
|
|
|
|
|
|
|
|
return list(seen.values())
|
2026-02-22 09:01:10 +01:00
|
|
|
|
|
|
|
|
|
2026-02-24 19:00:12 +01:00
|
|
|
def _parse_proxy_url(proxy: str) -> dict[str, Any]:
|
|
|
|
|
"""Parse proxy URL, extracting credentials into separate Playwright fields.
|
|
|
|
|
|
|
|
|
|
Handles: http://user:pass@host:port -> {server: "http://host:port", username: "user", password: "pass"}
|
2026-03-09 04:12:54 +01:00
|
|
|
Also handles: no credentials, URL-encoded special chars, socks5://, missing port,
|
|
|
|
|
and bare proxy strings without a scheme (e.g. 'user:pass@host:port' -> treated as http).
|
2026-02-24 19:00:12 +01:00
|
|
|
"""
|
2026-03-09 04:12:54 +01:00
|
|
|
# Bare format: "user:pass@host:port" — urlparse needs a scheme to extract credentials.
|
|
|
|
|
normalized = proxy
|
|
|
|
|
if "@" in proxy and "://" not in proxy:
|
|
|
|
|
normalized = f"http://{proxy}"
|
|
|
|
|
|
|
|
|
|
parsed = urlparse(normalized)
|
2026-02-24 19:00:12 +01:00
|
|
|
|
|
|
|
|
if not parsed.username:
|
2026-03-09 04:12:54 +01:00
|
|
|
return {"server": proxy} # no creds — return original unchanged
|
2026-02-24 19:00:12 +01:00
|
|
|
|
|
|
|
|
# Rebuild server URL without credentials
|
|
|
|
|
netloc = parsed.hostname or ""
|
|
|
|
|
if parsed.port:
|
|
|
|
|
netloc += f":{parsed.port}"
|
|
|
|
|
|
|
|
|
|
server = urlunparse((parsed.scheme, netloc, parsed.path, "", "", ""))
|
|
|
|
|
|
|
|
|
|
result: dict[str, Any] = {"server": server}
|
|
|
|
|
result["username"] = unquote(parsed.username)
|
|
|
|
|
if parsed.password:
|
|
|
|
|
result["password"] = unquote(parsed.password)
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
2026-03-04 20:11:30 +01:00
|
|
|
def _build_proxy_kwargs(proxy: str | ProxySettings | None) -> dict[str, Any]:
|
2026-02-22 09:01:10 +01:00
|
|
|
"""Build proxy kwargs for Playwright launch."""
|
|
|
|
|
if proxy is None:
|
|
|
|
|
return {}
|
2026-03-04 20:11:30 +01:00
|
|
|
if isinstance(proxy, dict):
|
|
|
|
|
return {"proxy": proxy}
|
2026-02-24 19:00:12 +01:00
|
|
|
return {"proxy": _parse_proxy_url(proxy)}
|