diff --git a/pythonlib/README.md b/pythonlib/README.md
index d2fd5e9..e62e9f3 100644
--- a/pythonlib/README.md
+++ b/pythonlib/README.md
@@ -45,7 +45,9 @@ Options:
Commands:
fetch Fetch the latest version of Camoufox
+ path Display the path to the Camoufox executable
remove Remove all downloaded files
+ server Launch a Playwright server
test Open the Playwright inspector
version Display the current version
```
@@ -192,6 +194,53 @@ with Camoufox(
+### Remote Server (experimental)
+
+> [!WARNING]
+> This feature is experimental and not meant for production use. It uses a hacky workaround to gain access to undocumented Playwright methods.
+
+#### Launching
+
+To launch a remote server, run the following CLI command:
+
+```bash
+python -m camoufox server
+```
+
+Or, configure the server with a launch script:
+
+```python
+from camoufox.server import launch_server
+
+launch_server(
+ headless=True,
+ geoip=True,
+ proxy={
+ 'server': 'http://example.com:8080',
+ 'username': 'username',
+ 'password': 'password'
+ }
+)
+```
+
+#### Connecting
+
+To connect to the remote server, use Playwright's `connect` method:
+
+```python
+from playwright.sync_api import sync_playwright
+
+with sync_playwright() as p:
+ # Example endpoint
+ browser = p.firefox.connect('ws://localhost:34091/8c7c6cdea3368d937ef7db2277d6647b')
+ page = browser.new_page()
+ ...
+```
+
+**Note:** Because servers only use **one browser instance**, fingerprints will not rotate between sessions. If you plan on using Camoufox at scale, consider rotating the server between sessions.
+
+
+
### BrowserForge Integration
Camoufox is compatible with [BrowserForge](https://github.com/daijro/browserforge) fingerprints.
diff --git a/pythonlib/camoufox/__main__.py b/pythonlib/camoufox/__main__.py
index e063fe7..637c3f5 100644
--- a/pythonlib/camoufox/__main__.py
+++ b/pythonlib/camoufox/__main__.py
@@ -104,6 +104,16 @@ def test(url: Optional[str] = None) -> None:
page.pause() # Open the Playwright inspector
+@cli.command(name='server')
+def server() -> None:
+ """
+ Launch a Playwright server
+ """
+ from .server import launch_server
+
+ launch_server()
+
+
@cli.command(name='path')
def path() -> None:
"""
diff --git a/pythonlib/camoufox/launchServer.js b/pythonlib/camoufox/launchServer.js
new file mode 100644
index 0000000..90a0aa2
--- /dev/null
+++ b/pythonlib/camoufox/launchServer.js
@@ -0,0 +1,40 @@
+// Workaround that accesses Playwright's undocumented `launchServer` method in Python
+// Without having to use the Node.js Playwright library.
+
+const { BrowserServerLauncherImpl } = require(`${process.cwd()}/lib/browserServerImpl.js`)
+
+function collectData() {
+ return new Promise((resolve) => {
+ let data = '';
+ process.stdin.setEncoding('utf8');
+
+ process.stdin.on('data', (chunk) => {
+ data += chunk;
+ });
+
+ process.stdin.on('end', () => {
+ resolve(JSON.parse(data));
+ });
+ });
+}
+
+collectData().then((options) => {
+ console.time('Server launched');
+ console.info('Launching server...');
+
+ const server = new BrowserServerLauncherImpl('firefox')
+
+ // Call Playwright's `launchServer` method
+ server.launchServer(options).then(browserServer => {
+ console.timeEnd('Server launched');
+ console.log('Websocket endpoint:\x1b[93m', browserServer.wsEndpoint(), '\x1b[0m');
+ // Continue forever
+ process.stdin.resume();
+ }).catch(error => {
+ console.error('Error launching server:', error.message);
+ process.exit(1);
+ });
+}).catch((error) => {
+ console.error('Error collecting data:', error.message);
+ process.exit(1); // Exit with error code
+});
\ No newline at end of file
diff --git a/pythonlib/camoufox/locale.py b/pythonlib/camoufox/locale.py
index 91e3ba8..1105967 100644
--- a/pythonlib/camoufox/locale.py
+++ b/pythonlib/camoufox/locale.py
@@ -1,14 +1,12 @@
-import os
import xml.etree.ElementTree as ET # nosec
from dataclasses import dataclass
-from pathlib import Path
from random import choice as randchoice
from typing import Any, Dict, List, Optional, Tuple, cast
import numpy as np
from language_tags import tags
-from camoufox.pkgman import rprint, webdl
+from camoufox.pkgman import LOCAL_DATA, rprint, webdl
from .exceptions import NotInstalledGeoIPExtra, UnknownIPLocation, UnknownTerritory
from .ip import validate_ip
@@ -20,8 +18,6 @@ except ImportError:
else:
ALLOW_GEOIP = True
-LOCAL_DATA = Path(os.path.abspath(__file__)).parent
-
"""
Data structures for locale and geolocation info
diff --git a/pythonlib/camoufox/pkgman.py b/pythonlib/camoufox/pkgman.py
index 1ccf353..dbdef4b 100644
--- a/pythonlib/camoufox/pkgman.py
+++ b/pythonlib/camoufox/pkgman.py
@@ -42,6 +42,7 @@ if sys.platform not in OS_MAP:
OS_NAME: Literal['mac', 'win', 'lin'] = OS_MAP[sys.platform]
INSTALL_DIR: Path = Path(user_cache_dir("camoufox"))
+LOCAL_DATA: Path = Path(os.path.abspath(__file__)).parent
# The supported architectures for each OS
OS_ARCH_MATRIX: dict[str, List[str]] = {
diff --git a/pythonlib/camoufox/server.py b/pythonlib/camoufox/server.py
new file mode 100644
index 0000000..4e32305
--- /dev/null
+++ b/pythonlib/camoufox/server.py
@@ -0,0 +1,70 @@
+import subprocess
+from pathlib import Path
+from typing import Any, Dict, NoReturn, Tuple, Union
+
+import orjson
+from playwright._impl._driver import compute_driver_executable
+
+from camoufox.pkgman import LOCAL_DATA
+from camoufox.utils import get_launch_options
+
+LAUNCH_SCRIPT: Path = LOCAL_DATA / "launchServer.js"
+
+
+def camel_case(snake_str: str) -> str:
+ """
+ Convert a string to camelCase
+ """
+ if len(snake_str) < 2:
+ return snake_str
+ camel_case_str = ''.join(x.capitalize() for x in snake_str.lower().split('_'))
+ return camel_case_str[0].lower() + camel_case_str[1:]
+
+
+def to_camel_case_dict(data: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ Convert a dictionary to camelCase
+ """
+ return {camel_case(key): value for key, value in data.items()}
+
+
+def get_nodejs() -> str:
+ """
+ Get the bundled Node.js executable
+ """
+ # Note: Older versions of Playwright return a string rather than a tuple.
+ _nodejs: Union[str, Tuple[str, ...]] = compute_driver_executable()[0]
+ if isinstance(_nodejs, tuple):
+ return _nodejs[0]
+ return _nodejs
+
+
+def launch_server(**kwargs) -> NoReturn:
+ """
+ Launch a Playwright server. Takes the same arguments as `Camoufox()`.
+ Prints the websocket endpoint to the console.
+ """
+ config = get_launch_options(**kwargs)
+ nodejs = get_nodejs()
+
+ data = orjson.dumps(to_camel_case_dict(config)).decode()
+
+ process = subprocess.Popen( # nosec
+ [
+ nodejs,
+ str(LAUNCH_SCRIPT),
+ ],
+ cwd=Path(nodejs).parent / "package",
+ stdin=subprocess.PIPE,
+ text=True,
+ )
+ # Write data to stdin and close the stream
+ if process.stdin:
+ process.stdin.write(data)
+ process.stdin.close()
+
+ # Wait forever
+ process.wait()
+
+ # Add an explicit return statement to satisfy the NoReturn type hint
+ raise RuntimeError("Server process terminated unexpectedly")
diff --git a/pythonlib/pyproject.toml b/pythonlib/pyproject.toml
index 46fdc1e..668c0fc 100644
--- a/pythonlib/pyproject.toml
+++ b/pythonlib/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "camoufox"
-version = "0.2.3"
+version = "0.2.4"
description = "Wrapper around Playwright to help launch Camoufox"
authors = ["daijro "]
license = "MIT"