mirror of
https://forge.fsky.io/oneflux/omegafox.git
synced 2026-02-10 08:12:05 -08:00
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:
parent
7985eec493
commit
79c436e506
7 changed files with 172 additions and 6 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
40
pythonlib/camoufox/launchServer.js
Normal file
40
pythonlib/camoufox/launchServer.js
Normal 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
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]] = {
|
||||||
|
|
|
||||||
70
pythonlib/camoufox/server.py
Normal file
70
pythonlib/camoufox/server.py
Normal 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")
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue