mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
Merge pull request #22 from evelaa123/feat/launch-persistent-context
feat: add launchPersistentContext() to avoid incognito detection
This commit is contained in:
@@ -11,7 +11,7 @@ Usage:
|
|||||||
browser.close()
|
browser.close()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .browser import launch, launch_async, launch_context
|
from .browser import launch, launch_async, launch_context, launch_persistent_context, launch_persistent_context_async
|
||||||
from .config import CHROMIUM_VERSION, get_default_stealth_args
|
from .config import CHROMIUM_VERSION, get_default_stealth_args
|
||||||
from .download import binary_info, check_for_update, clear_cache, ensure_binary
|
from .download import binary_info, check_for_update, clear_cache, ensure_binary
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
@@ -20,6 +20,8 @@ __all__ = [
|
|||||||
"launch",
|
"launch",
|
||||||
"launch_async",
|
"launch_async",
|
||||||
"launch_context",
|
"launch_context",
|
||||||
|
"launch_persistent_context",
|
||||||
|
"launch_persistent_context_async",
|
||||||
"ensure_binary",
|
"ensure_binary",
|
||||||
"clear_cache",
|
"clear_cache",
|
||||||
"binary_info",
|
"binary_info",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Usage:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from urllib.parse import unquote, urlparse, urlunparse
|
from urllib.parse import unquote, urlparse, urlunparse
|
||||||
|
|
||||||
@@ -159,6 +160,200 @@ async def launch_async(
|
|||||||
return browser
|
return browser
|
||||||
|
|
||||||
|
|
||||||
|
def launch_persistent_context(
|
||||||
|
user_data_dir: str | os.PathLike,
|
||||||
|
headless: bool = True,
|
||||||
|
proxy: str | None = None,
|
||||||
|
args: list[str] | None = None,
|
||||||
|
stealth_args: bool = True,
|
||||||
|
user_agent: str | None = None,
|
||||||
|
viewport: dict | None = None,
|
||||||
|
locale: str | None = None,
|
||||||
|
timezone_id: str | None = None,
|
||||||
|
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
|
||||||
|
geoip: bool = False,
|
||||||
|
**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).
|
||||||
|
proxy: Proxy server URL (e.g. 'http://proxy:8080' or 'socks5://proxy:1080').
|
||||||
|
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".
|
||||||
|
timezone_id: Timezone, e.g. "America/New_York".
|
||||||
|
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]``.
|
||||||
|
**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.
|
||||||
|
"""
|
||||||
|
from patchright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
binary_path = ensure_binary()
|
||||||
|
timezone_id, locale = _maybe_resolve_geoip(geoip, proxy, timezone_id, locale)
|
||||||
|
chrome_args = _build_args(stealth_args, args, timezone=timezone_id, locale=locale)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
|
||||||
|
headless,
|
||||||
|
user_data_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
context_kwargs: dict[str, Any] = {}
|
||||||
|
if user_agent:
|
||||||
|
context_kwargs["user_agent"] = user_agent
|
||||||
|
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
|
||||||
|
if locale:
|
||||||
|
context_kwargs["locale"] = locale
|
||||||
|
if timezone_id:
|
||||||
|
context_kwargs["timezone_id"] = timezone_id
|
||||||
|
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,
|
||||||
|
ignore_default_args=["--enable-automation"],
|
||||||
|
**_build_proxy_kwargs(proxy),
|
||||||
|
**context_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Patch close() to also stop the Playwright instance
|
||||||
|
_original_close = context.close
|
||||||
|
|
||||||
|
def _close_with_cleanup() -> None:
|
||||||
|
_original_close()
|
||||||
|
pw.stop()
|
||||||
|
|
||||||
|
context.close = _close_with_cleanup
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
async def launch_persistent_context_async(
|
||||||
|
user_data_dir: str | os.PathLike,
|
||||||
|
headless: bool = True,
|
||||||
|
proxy: str | None = None,
|
||||||
|
args: list[str] | None = None,
|
||||||
|
stealth_args: bool = True,
|
||||||
|
user_agent: str | None = None,
|
||||||
|
viewport: dict | None = None,
|
||||||
|
locale: str | None = None,
|
||||||
|
timezone_id: str | None = None,
|
||||||
|
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
|
||||||
|
geoip: bool = False,
|
||||||
|
**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).
|
||||||
|
proxy: Proxy server URL (e.g. 'http://proxy:8080' or 'socks5://proxy:1080').
|
||||||
|
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".
|
||||||
|
timezone_id: Timezone, e.g. "America/New_York".
|
||||||
|
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
|
||||||
|
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
||||||
|
**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())
|
||||||
|
"""
|
||||||
|
from patchright.async_api import async_playwright
|
||||||
|
|
||||||
|
binary_path = ensure_binary()
|
||||||
|
timezone_id, locale = _maybe_resolve_geoip(geoip, proxy, timezone_id, locale)
|
||||||
|
chrome_args = _build_args(stealth_args, args, timezone=timezone_id, locale=locale)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
|
||||||
|
headless,
|
||||||
|
user_data_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
context_kwargs: dict[str, Any] = {}
|
||||||
|
if user_agent:
|
||||||
|
context_kwargs["user_agent"] = user_agent
|
||||||
|
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
|
||||||
|
if locale:
|
||||||
|
context_kwargs["locale"] = locale
|
||||||
|
if timezone_id:
|
||||||
|
context_kwargs["timezone_id"] = timezone_id
|
||||||
|
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,
|
||||||
|
ignore_default_args=["--enable-automation"],
|
||||||
|
**_build_proxy_kwargs(proxy),
|
||||||
|
**context_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Patch close() to also stop the Playwright instance
|
||||||
|
_original_close = context.close
|
||||||
|
|
||||||
|
async def _close_with_cleanup() -> None:
|
||||||
|
await _original_close()
|
||||||
|
await pw.stop()
|
||||||
|
|
||||||
|
context.close = _close_with_cleanup
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
def launch_context(
|
def launch_context(
|
||||||
headless: bool = True,
|
headless: bool = True,
|
||||||
proxy: str | None = None,
|
proxy: str | None = None,
|
||||||
|
|||||||
+2
-2
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Launch functions (Playwright API)
|
// Launch functions (Playwright API)
|
||||||
export { launch, launchContext } from "./playwright.js";
|
export { launch, launchContext, launchPersistentContext } from "./playwright.js";
|
||||||
|
|
||||||
// Binary management
|
// Binary management
|
||||||
export { ensureBinary, clearCache, binaryInfo, checkForUpdate } from "./download.js";
|
export { ensureBinary, clearCache, binaryInfo, checkForUpdate } from "./download.js";
|
||||||
@@ -25,4 +25,4 @@ export { ensureBinary, clearCache, binaryInfo, checkForUpdate } from "./download
|
|||||||
export { CHROMIUM_VERSION, getDefaultStealthArgs } from "./config.js";
|
export { CHROMIUM_VERSION, getDefaultStealthArgs } from "./config.js";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type { LaunchOptions, LaunchContextOptions, BinaryInfo } from "./types.js";
|
export type { LaunchOptions, LaunchContextOptions, LaunchPersistentContextOptions, BinaryInfo } from "./types.js";
|
||||||
|
|||||||
+48
-1
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Browser, BrowserContext } from "playwright-core";
|
import type { Browser, BrowserContext } from "playwright-core";
|
||||||
import type { LaunchOptions, LaunchContextOptions } from "./types.js";
|
import type { LaunchOptions, LaunchContextOptions, LaunchPersistentContextOptions } from "./types.js";
|
||||||
import { DEFAULT_VIEWPORT, getDefaultStealthArgs } from "./config.js";
|
import { DEFAULT_VIEWPORT, getDefaultStealthArgs } from "./config.js";
|
||||||
import { ensureBinary } from "./download.js";
|
import { ensureBinary } from "./download.js";
|
||||||
import { parseProxyUrl } from "./proxy.js";
|
import { parseProxyUrl } from "./proxy.js";
|
||||||
@@ -88,6 +88,53 @@ export async function launchContext(
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch stealth browser with a persistent user profile (non-incognito).
|
||||||
|
* Uses Playwright's chromium.launchPersistentContext() under the hood.
|
||||||
|
*
|
||||||
|
* This avoids incognito detection by services like BrowserScan (-10% penalty)
|
||||||
|
* and enables session persistence (cookies, localStorage) across launches.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { launchPersistentContext } from 'cloakbrowser';
|
||||||
|
* const context = await launchPersistentContext({
|
||||||
|
* userDataDir: './chrome-profile',
|
||||||
|
* headless: false,
|
||||||
|
* proxy: 'http://user:pass@host:port',
|
||||||
|
* geoip: true,
|
||||||
|
* });
|
||||||
|
* const page = context.pages()[0] || await context.newPage();
|
||||||
|
* await page.goto('https://example.com');
|
||||||
|
* await context.close();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function launchPersistentContext(
|
||||||
|
options: LaunchPersistentContextOptions
|
||||||
|
): Promise<BrowserContext> {
|
||||||
|
const { chromium } = await import("playwright-core");
|
||||||
|
|
||||||
|
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
|
||||||
|
const resolved = await maybeResolveGeoip(options);
|
||||||
|
const args = buildArgs({ ...options, ...resolved });
|
||||||
|
|
||||||
|
const context = await chromium.launchPersistentContext(options.userDataDir, {
|
||||||
|
executablePath: binaryPath,
|
||||||
|
headless: options.headless ?? true,
|
||||||
|
args,
|
||||||
|
ignoreDefaultArgs: ["--enable-automation"],
|
||||||
|
...(options.proxy ? { proxy: parseProxyUrl(options.proxy) } : {}),
|
||||||
|
...(options.userAgent ? { userAgent: options.userAgent } : {}),
|
||||||
|
viewport: options.viewport ?? DEFAULT_VIEWPORT,
|
||||||
|
...(resolved.locale ? { locale: resolved.locale } : {}),
|
||||||
|
...(resolved.timezone ? { timezoneId: resolved.timezone } : {}),
|
||||||
|
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
|
||||||
|
...options.launchOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Internal
|
// Internal
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export interface LaunchContextOptions extends LaunchOptions {
|
|||||||
colorScheme?: "light" | "dark" | "no-preference";
|
colorScheme?: "light" | "dark" | "no-preference";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LaunchPersistentContextOptions extends LaunchContextOptions {
|
||||||
|
/** Path to user data directory for persistent profile. */
|
||||||
|
userDataDir: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BinaryInfo {
|
export interface BinaryInfo {
|
||||||
version: string;
|
version: string;
|
||||||
platform: string;
|
platform: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user