pythonlib: Add remote server launching #7

Uses a hacky work around to run Javascript in Playwright's internal library to gain access to the launchServer method.
This commit is contained in:
daijro 2024-10-02 02:45:26 -05:00
parent 7985eec493
commit 79c436e506
7 changed files with 172 additions and 6 deletions

View file

@ -45,7 +45,9 @@ Options:
Commands: Commands:
fetch Fetch the latest version of Camoufox fetch Fetch the latest version of Camoufox
path Display the path to the Camoufox executable
remove Remove all downloaded files remove Remove all downloaded files
server Launch a Playwright server
test Open the Playwright inspector test Open the Playwright inspector
version Display the current version version Display the current version
``` ```
@ -192,6 +194,53 @@ with Camoufox(
<hr width=50> <hr width=50>
### 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.
<hr width=50>
### BrowserForge Integration ### BrowserForge Integration
Camoufox is compatible with [BrowserForge](https://github.com/daijro/browserforge) fingerprints. Camoufox is compatible with [BrowserForge](https://github.com/daijro/browserforge) fingerprints.

View file

@ -104,6 +104,16 @@ def test(url: Optional[str] = None) -> None:
page.pause() # Open the Playwright inspector 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') @cli.command(name='path')
def path() -> None: def path() -> None:
""" """

View file

@ -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
});

View file

@ -1,14 +1,12 @@
import os
import xml.etree.ElementTree as ET # nosec import xml.etree.ElementTree as ET # nosec
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from random import choice as randchoice from random import choice as randchoice
from typing import Any, Dict, List, Optional, Tuple, cast from typing import Any, Dict, List, Optional, Tuple, cast
import numpy as np import numpy as np
from language_tags import tags 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 .exceptions import NotInstalledGeoIPExtra, UnknownIPLocation, UnknownTerritory
from .ip import validate_ip from .ip import validate_ip
@ -20,8 +18,6 @@ except ImportError:
else: else:
ALLOW_GEOIP = True ALLOW_GEOIP = True
LOCAL_DATA = Path(os.path.abspath(__file__)).parent
""" """
Data structures for locale and geolocation info Data structures for locale and geolocation info

View file

@ -42,6 +42,7 @@ if sys.platform not in OS_MAP:
OS_NAME: Literal['mac', 'win', 'lin'] = OS_MAP[sys.platform] OS_NAME: Literal['mac', 'win', 'lin'] = OS_MAP[sys.platform]
INSTALL_DIR: Path = Path(user_cache_dir("camoufox")) INSTALL_DIR: Path = Path(user_cache_dir("camoufox"))
LOCAL_DATA: Path = Path(os.path.abspath(__file__)).parent
# The supported architectures for each OS # The supported architectures for each OS
OS_ARCH_MATRIX: dict[str, List[str]] = { OS_ARCH_MATRIX: dict[str, List[str]] = {

View file

@ -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")

View file

@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry] [tool.poetry]
name = "camoufox" name = "camoufox"
version = "0.2.3" version = "0.2.4"
description = "Wrapper around Playwright to help launch Camoufox" description = "Wrapper around Playwright to help launch Camoufox"
authors = ["daijro <daijro.dev@gmail.com>"] authors = ["daijro <daijro.dev@gmail.com>"]
license = "MIT" license = "MIT"