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:
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(
<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
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
@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:
"""

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
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

View file

@ -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]] = {

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]
name = "camoufox"
version = "0.2.3"
version = "0.2.4"
description = "Wrapper around Playwright to help launch Camoufox"
authors = ["daijro <daijro.dev@gmail.com>"]
license = "MIT"