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"