mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
feat: add extension_paths parameter for loading Chrome extensions (#210)
Add `extension_paths` parameter to all launch functions (Python + JS) for loading Chrome extensions. Resolves paths to absolute, injects `--load-extension` and `--disable-extensions-except` flags via `build_args()`. Note: Extensions require a persistent context (`launch_persistent_context`) to function — this is a Chromium limitation. Co-authored-by: zackycodes <75211659+zackycodes@users.noreply.github.com>
This commit is contained in:
+29
-6
@@ -64,6 +64,7 @@ def launch(
|
|||||||
humanize: bool = False,
|
humanize: bool = False,
|
||||||
human_preset: HumanPreset = "default",
|
human_preset: HumanPreset = "default",
|
||||||
human_config: HumanConfigOverrides | None = None,
|
human_config: HumanConfigOverrides | None = None,
|
||||||
|
extension_paths: list[str] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Launch stealth Chromium browser. Returns a Playwright Browser object.
|
"""Launch stealth Chromium browser. Returns a Playwright Browser object.
|
||||||
@@ -75,6 +76,7 @@ def launch(
|
|||||||
Dict: {"server": "http://proxy:8080", "bypass": ".google.com", ...}
|
Dict: {"server": "http://proxy:8080", "bypass": ".google.com", ...}
|
||||||
— passed directly to Playwright.
|
— passed directly to Playwright.
|
||||||
args: Additional Chromium CLI arguments to pass.
|
args: Additional Chromium CLI arguments to pass.
|
||||||
|
extension_paths: List of Chrome extension paths to load.
|
||||||
stealth_args: Include default stealth fingerprint args (default True).
|
stealth_args: Include default stealth fingerprint args (default True).
|
||||||
Set to False if you want to pass your own --fingerprint flags.
|
Set to False if you want to pass your own --fingerprint flags.
|
||||||
timezone: IANA timezone (e.g. 'America/New_York'). Sets --fingerprint-timezone binary flag.
|
timezone: IANA timezone (e.g. 'America/New_York'). Sets --fingerprint-timezone binary flag.
|
||||||
@@ -112,7 +114,8 @@ def launch(
|
|||||||
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
||||||
args = list(args or [])
|
args = list(args or [])
|
||||||
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
||||||
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
|
|
||||||
|
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths)
|
||||||
|
|
||||||
logger.debug("Launching stealth Chromium (headless=%s, args=%d)", headless, len(chrome_args))
|
logger.debug("Launching stealth Chromium (headless=%s, args=%d)", headless, len(chrome_args))
|
||||||
|
|
||||||
@@ -159,6 +162,7 @@ async def launch_async( # noqa: C901
|
|||||||
humanize: bool = False,
|
humanize: bool = False,
|
||||||
human_preset: HumanPreset = "default",
|
human_preset: HumanPreset = "default",
|
||||||
human_config: HumanConfigOverrides | None = None,
|
human_config: HumanConfigOverrides | None = None,
|
||||||
|
extension_paths: list[str] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Async version of launch(). Returns a Playwright Browser object.
|
"""Async version of launch(). Returns a Playwright Browser object.
|
||||||
@@ -167,6 +171,7 @@ async def launch_async( # noqa: C901
|
|||||||
headless: Run in headless mode (default True).
|
headless: Run in headless mode (default True).
|
||||||
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
||||||
args: Additional Chromium CLI arguments to pass.
|
args: Additional Chromium CLI arguments to pass.
|
||||||
|
extension_paths: List of Chrome extension paths to load.
|
||||||
stealth_args: Include default stealth fingerprint args (default True).
|
stealth_args: Include default stealth fingerprint args (default True).
|
||||||
timezone: IANA timezone (e.g. 'America/New_York'). Sets --fingerprint-timezone binary flag.
|
timezone: IANA timezone (e.g. 'America/New_York'). Sets --fingerprint-timezone binary flag.
|
||||||
locale: BCP 47 locale (e.g. 'en-US'). Sets --lang binary flag.
|
locale: BCP 47 locale (e.g. 'en-US'). Sets --lang binary flag.
|
||||||
@@ -202,7 +207,7 @@ async def launch_async( # noqa: C901
|
|||||||
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
||||||
args = list(args or [])
|
args = list(args or [])
|
||||||
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
||||||
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
|
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths)
|
||||||
|
|
||||||
logger.debug("Launching stealth Chromium async (headless=%s, args=%d)", headless, len(chrome_args))
|
logger.debug("Launching stealth Chromium async (headless=%s, args=%d)", headless, len(chrome_args))
|
||||||
|
|
||||||
@@ -253,6 +258,7 @@ def launch_persistent_context(
|
|||||||
humanize: bool = False,
|
humanize: bool = False,
|
||||||
human_preset: HumanPreset = "default",
|
human_preset: HumanPreset = "default",
|
||||||
human_config: HumanConfigOverrides | None = None,
|
human_config: HumanConfigOverrides | None = None,
|
||||||
|
extension_paths: list[str] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Launch stealth browser with a persistent profile and return a BrowserContext.
|
"""Launch stealth browser with a persistent profile and return a BrowserContext.
|
||||||
@@ -268,6 +274,7 @@ def launch_persistent_context(
|
|||||||
headless: Run in headless mode (default True).
|
headless: Run in headless mode (default True).
|
||||||
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
||||||
args: Additional Chromium CLI arguments.
|
args: Additional Chromium CLI arguments.
|
||||||
|
extension_paths: List of Chrome extension paths to load.
|
||||||
stealth_args: Include default stealth fingerprint args (default True).
|
stealth_args: Include default stealth fingerprint args (default True).
|
||||||
user_agent: Custom user agent string.
|
user_agent: Custom user agent string.
|
||||||
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
||||||
@@ -306,7 +313,7 @@ def launch_persistent_context(
|
|||||||
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
||||||
args = list(args or [])
|
args = list(args or [])
|
||||||
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
||||||
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
|
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
|
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
|
||||||
@@ -377,6 +384,7 @@ async def launch_persistent_context_async(
|
|||||||
humanize: bool = False,
|
humanize: bool = False,
|
||||||
human_preset: HumanPreset = "default",
|
human_preset: HumanPreset = "default",
|
||||||
human_config: HumanConfigOverrides | None = None,
|
human_config: HumanConfigOverrides | None = None,
|
||||||
|
extension_paths: list[str] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Async version of launch_persistent_context().
|
"""Async version of launch_persistent_context().
|
||||||
@@ -391,6 +399,7 @@ async def launch_persistent_context_async(
|
|||||||
headless: Run in headless mode (default True).
|
headless: Run in headless mode (default True).
|
||||||
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
||||||
args: Additional Chromium CLI arguments.
|
args: Additional Chromium CLI arguments.
|
||||||
|
extension_paths: List of Chrome extension paths to load.
|
||||||
stealth_args: Include default stealth fingerprint args (default True).
|
stealth_args: Include default stealth fingerprint args (default True).
|
||||||
user_agent: Custom user agent string.
|
user_agent: Custom user agent string.
|
||||||
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
||||||
@@ -432,7 +441,7 @@ async def launch_persistent_context_async(
|
|||||||
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
|
||||||
args = list(args or [])
|
args = list(args or [])
|
||||||
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
|
||||||
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless)
|
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
|
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
|
||||||
@@ -502,6 +511,7 @@ def launch_context(
|
|||||||
humanize: bool = False,
|
humanize: bool = False,
|
||||||
human_preset: HumanPreset = "default",
|
human_preset: HumanPreset = "default",
|
||||||
human_config: HumanConfigOverrides | None = None,
|
human_config: HumanConfigOverrides | None = None,
|
||||||
|
extension_paths: list[str] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Launch stealth browser and return a BrowserContext with common options pre-set.
|
"""Launch stealth browser and return a BrowserContext with common options pre-set.
|
||||||
@@ -513,6 +523,7 @@ def launch_context(
|
|||||||
headless: Run in headless mode (default True).
|
headless: Run in headless mode (default True).
|
||||||
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
||||||
args: Additional Chromium CLI arguments.
|
args: Additional Chromium CLI arguments.
|
||||||
|
extension_paths: List of Chrome extension paths to load.
|
||||||
stealth_args: Include default stealth fingerprint args (default True).
|
stealth_args: Include default stealth fingerprint args (default True).
|
||||||
user_agent: Custom user agent string.
|
user_agent: Custom user agent string.
|
||||||
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
||||||
@@ -544,7 +555,7 @@ def launch_context(
|
|||||||
# so it applies to ALL contexts, not just the default one.
|
# so it applies to ALL contexts, not just the default one.
|
||||||
# locale and timezone are set via binary flags only — no CDP emulation.
|
# locale and timezone are set via binary flags only — no CDP emulation.
|
||||||
browser = launch(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args,
|
browser = launch(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args,
|
||||||
timezone=timezone, locale=locale, backend=backend)
|
timezone=timezone, locale=locale, backend=backend, extension_paths=extension_paths)
|
||||||
|
|
||||||
context_kwargs: dict[str, Any] = {}
|
context_kwargs: dict[str, Any] = {}
|
||||||
if user_agent:
|
if user_agent:
|
||||||
@@ -601,6 +612,7 @@ async def launch_context_async(
|
|||||||
humanize: bool = False,
|
humanize: bool = False,
|
||||||
human_preset: HumanPreset = "default",
|
human_preset: HumanPreset = "default",
|
||||||
human_config: HumanConfigOverrides | None = None,
|
human_config: HumanConfigOverrides | None = None,
|
||||||
|
extension_paths: list[str] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Async version of launch_context().
|
"""Async version of launch_context().
|
||||||
@@ -614,6 +626,7 @@ async def launch_context_async(
|
|||||||
headless: Run in headless mode (default True).
|
headless: Run in headless mode (default True).
|
||||||
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
proxy: Proxy URL string or Playwright proxy dict (see launch() for details).
|
||||||
args: Additional Chromium CLI arguments.
|
args: Additional Chromium CLI arguments.
|
||||||
|
extension_paths: List of Chrome extension paths to load.
|
||||||
stealth_args: Include default stealth fingerprint args (default True).
|
stealth_args: Include default stealth fingerprint args (default True).
|
||||||
user_agent: Custom user agent string.
|
user_agent: Custom user agent string.
|
||||||
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
||||||
@@ -662,7 +675,7 @@ async def launch_context_async(
|
|||||||
# so it applies to ALL contexts, not just the default one.
|
# so it applies to ALL contexts, not just the default one.
|
||||||
# locale and timezone are set via binary flags only — no CDP emulation.
|
# locale and timezone are set via binary flags only — no CDP emulation.
|
||||||
browser = await launch_async(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args,
|
browser = await launch_async(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args,
|
||||||
timezone=timezone, locale=locale, backend=backend)
|
timezone=timezone, locale=locale, backend=backend, extension_paths=extension_paths)
|
||||||
|
|
||||||
context_kwargs: dict[str, Any] = {}
|
context_kwargs: dict[str, Any] = {}
|
||||||
if user_agent:
|
if user_agent:
|
||||||
@@ -957,6 +970,7 @@ def build_args(
|
|||||||
timezone: str | None = None,
|
timezone: str | None = None,
|
||||||
locale: str | None = None,
|
locale: str | None = None,
|
||||||
headless: bool = True,
|
headless: bool = True,
|
||||||
|
extension_paths: list[str] | None = None,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Combine stealth args with user-provided args and locale flags.
|
"""Combine stealth args with user-provided args and locale flags.
|
||||||
|
|
||||||
@@ -1000,6 +1014,15 @@ def build_args(
|
|||||||
logger.debug("Arg override: %s -> %s", seen[key], flag)
|
logger.debug("Arg override: %s -> %s", seen[key], flag)
|
||||||
seen[key] = flag
|
seen[key] = flag
|
||||||
|
|
||||||
|
if extension_paths:
|
||||||
|
abs_paths = [os.path.abspath(p) for p in extension_paths]
|
||||||
|
ext_val = ",".join(abs_paths)
|
||||||
|
|
||||||
|
seen["--load-extension"] = f"--load-extension={ext_val}"
|
||||||
|
seen["--disable-extensions-except"] = (
|
||||||
|
f"--disable-extensions-except={ext_val}"
|
||||||
|
)
|
||||||
|
|
||||||
return list(seen.values())
|
return list(seen.values())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Shared argument builder for Playwright and Puppeteer wrappers.
|
* Shared argument builder for Playwright and Puppeteer wrappers.
|
||||||
*/
|
*/
|
||||||
|
import path from "path";
|
||||||
import type { LaunchOptions } from "./types.js";
|
import type { LaunchOptions } from "./types.js";
|
||||||
import { getDefaultStealthArgs } from "./config.js";
|
import { getDefaultStealthArgs } from "./config.js";
|
||||||
|
|
||||||
@@ -55,5 +55,16 @@ export function buildArgs(options: LaunchOptions): string[] {
|
|||||||
seen.set(k, flag);
|
seen.set(k, flag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.extensionPaths?.length) {
|
||||||
|
const absPaths = options.extensionPaths.map(p => path.resolve(p));
|
||||||
|
const joined = absPaths.join(",");
|
||||||
|
|
||||||
|
seen.set("--load-extension", `--load-extension=${joined}`);
|
||||||
|
seen.set(
|
||||||
|
"--disable-extensions-except",
|
||||||
|
`--disable-extensions-except=${joined}`
|
||||||
|
);
|
||||||
|
}
|
||||||
return [...seen.values()];
|
return [...seen.values()];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export interface LaunchOptions {
|
|||||||
proxy?: string | { server: string; bypass?: string; username?: string; password?: string };
|
proxy?: string | { server: string; bypass?: string; username?: string; password?: string };
|
||||||
/** Additional Chromium CLI arguments. */
|
/** Additional Chromium CLI arguments. */
|
||||||
args?: string[];
|
args?: string[];
|
||||||
|
/** Chrome extension paths to load. */
|
||||||
|
extensionPaths?: string[];
|
||||||
/** Include default stealth fingerprint args (default: true). Set false to use custom --fingerprint flags. */
|
/** Include default stealth fingerprint args (default: true). Set false to use custom --fingerprint flags. */
|
||||||
stealthArgs?: boolean;
|
stealthArgs?: boolean;
|
||||||
/** IANA timezone, e.g. "America/New_York". Sets --fingerprint-timezone binary flag. */
|
/** IANA timezone, e.g. "America/New_York". Sets --fingerprint-timezone binary flag. */
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import path from "path";
|
||||||
|
import { _buildArgsForTest } from "../src/playwright.js";
|
||||||
|
|
||||||
|
test("extension paths inject chrome flags", () => {
|
||||||
|
const args = _buildArgsForTest({
|
||||||
|
extensionPaths: ["./ext"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const abs = path.resolve("./ext");
|
||||||
|
|
||||||
|
expect(args).toContain(`--load-extension=${abs}`);
|
||||||
|
|
||||||
|
expect(args).toContain(
|
||||||
|
`--disable-extensions-except=${abs}`
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from cloakbrowser import launch
|
||||||
|
|
||||||
|
|
||||||
|
@patch("cloakbrowser.browser.ensure_binary")
|
||||||
|
@patch("cloakbrowser.browser._import_sync_playwright")
|
||||||
|
def test_extension_loading(mock_playwright_import, mock_ensure_binary):
|
||||||
|
mock_ensure_binary.return_value = "/fake/chrome"
|
||||||
|
|
||||||
|
mock_browser = MagicMock()
|
||||||
|
|
||||||
|
mock_pw = MagicMock()
|
||||||
|
mock_pw.chromium.launch.return_value = mock_browser
|
||||||
|
|
||||||
|
mock_pw_manager = MagicMock()
|
||||||
|
mock_pw_manager.return_value.start.return_value = mock_pw
|
||||||
|
|
||||||
|
mock_playwright_import.return_value = mock_pw_manager
|
||||||
|
|
||||||
|
launch(extension_paths=["./ext"])
|
||||||
|
|
||||||
|
mock_pw.chromium.launch.assert_called_once()
|
||||||
|
|
||||||
|
launch_call = mock_pw.chromium.launch.call_args
|
||||||
|
|
||||||
|
args = launch_call.kwargs["args"]
|
||||||
|
|
||||||
|
abs_path = os.path.abspath("./ext")
|
||||||
|
|
||||||
|
assert f"--load-extension={abs_path}" in args
|
||||||
|
assert f"--disable-extensions-except={abs_path}" in args
|
||||||
Reference in New Issue
Block a user