camoufox 0.3.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- camoufox/__init__.py +13 -0
- camoufox/__main__.py +172 -0
- camoufox/__version__.py +19 -0
- camoufox/addons.py +189 -0
- camoufox/async_api.py +94 -0
- camoufox/browserforge.yml +67 -0
- camoufox/exceptions.py +180 -0
- camoufox/fingerprints.py +142 -0
- camoufox/fonts.json +11 -0
- camoufox/ip.py +116 -0
- camoufox/launchServer.js +40 -0
- camoufox/locale.py +374 -0
- camoufox/pkgman.py +459 -0
- camoufox/py.typed +1 -0
- camoufox/server.py +70 -0
- camoufox/setup.cfg +3 -0
- camoufox/sync_api.py +94 -0
- camoufox/territoryInfo.xml +2024 -0
- camoufox/utils.py +582 -0
- camoufox/virtdisplay.py +146 -0
- camoufox/warnings.py +44 -0
- camoufox/warnings.yml +42 -0
- camoufox/xpi_dl.py +67 -0
- camoufox-0.3.8.dist-info/METADATA +116 -0
- camoufox-0.3.8.dist-info/RECORD +27 -0
- camoufox-0.3.8.dist-info/WHEEL +4 -0
- camoufox-0.3.8.dist-info/entry_points.txt +3 -0
camoufox/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .addons import DefaultAddons
|
|
2
|
+
from .async_api import AsyncCamoufox, AsyncNewBrowser
|
|
3
|
+
from .sync_api import Camoufox, NewBrowser
|
|
4
|
+
from .utils import launch_options
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Camoufox",
|
|
8
|
+
"NewBrowser",
|
|
9
|
+
"AsyncCamoufox",
|
|
10
|
+
"AsyncNewBrowser",
|
|
11
|
+
"DefaultAddons",
|
|
12
|
+
"launch_options",
|
|
13
|
+
]
|
camoufox/__main__.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI package manager for Camoufox.
|
|
3
|
+
|
|
4
|
+
Adapted from https://github.com/daijro/hrequests/blob/main/hrequests/__main__.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import PackageNotFoundError
|
|
8
|
+
from importlib.metadata import version as pkg_version
|
|
9
|
+
from os import environ
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from .addons import DefaultAddons
|
|
15
|
+
from .locale import ALLOW_GEOIP, download_mmdb, remove_mmdb
|
|
16
|
+
from .pkgman import INSTALL_DIR, CamoufoxFetcher, installed_verstr, rprint
|
|
17
|
+
from .xpi_dl import maybe_download_addons
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from browserforge.download import download as update_browserforge
|
|
21
|
+
except ImportError:
|
|
22
|
+
# Account for other Browserforge versions
|
|
23
|
+
from browserforge.download import Download as update_browserforge
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CamoufoxUpdate(CamoufoxFetcher):
|
|
27
|
+
"""
|
|
28
|
+
Checks & updates Camoufox
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Initializes the CamoufoxUpdate class
|
|
34
|
+
"""
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.current_verstr: Optional[str]
|
|
37
|
+
try:
|
|
38
|
+
self.current_verstr = installed_verstr()
|
|
39
|
+
except FileNotFoundError:
|
|
40
|
+
self.current_verstr = None
|
|
41
|
+
|
|
42
|
+
def is_updated_needed(self) -> bool:
|
|
43
|
+
# Camoufox is not installed
|
|
44
|
+
if self.current_verstr is None:
|
|
45
|
+
return True
|
|
46
|
+
# If the installed version is not the latest version
|
|
47
|
+
if self.current_verstr != self.verstr:
|
|
48
|
+
return True
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def update(self) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Updates Camoufox if needed
|
|
54
|
+
"""
|
|
55
|
+
# Check if the version is the same as the latest available version
|
|
56
|
+
if not self.is_updated_needed():
|
|
57
|
+
rprint("Camoufox binaries up to date!", fg="green")
|
|
58
|
+
rprint(f"Current version: v{self.current_verstr}", fg="green")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# Download updated file
|
|
62
|
+
if self.current_verstr is not None:
|
|
63
|
+
# Display an updating message
|
|
64
|
+
rprint(
|
|
65
|
+
f"Updating Camoufox binaries from v{self.current_verstr} => v{self.verstr}",
|
|
66
|
+
fg="yellow",
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
rprint(f"Fetching Camoufox binaries v{self.verstr}...", fg="yellow")
|
|
70
|
+
# Install the new version
|
|
71
|
+
self.install()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@click.group()
|
|
75
|
+
def cli() -> None:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@cli.command(name='fetch')
|
|
80
|
+
@click.option(
|
|
81
|
+
'--browserforge', is_flag=True, help='Update browserforge\'s header and fingerprint definitions'
|
|
82
|
+
)
|
|
83
|
+
def fetch(browserforge=False) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Fetch the latest version of Camoufox and optionally update Browserforge's database
|
|
86
|
+
"""
|
|
87
|
+
CamoufoxUpdate().update()
|
|
88
|
+
# Fetch the GeoIP database
|
|
89
|
+
if ALLOW_GEOIP:
|
|
90
|
+
download_mmdb()
|
|
91
|
+
|
|
92
|
+
# Download default addons
|
|
93
|
+
maybe_download_addons(list(DefaultAddons))
|
|
94
|
+
|
|
95
|
+
if browserforge:
|
|
96
|
+
update_browserforge(headers=True, fingerprints=True)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@cli.command(name='remove')
|
|
100
|
+
def remove() -> None:
|
|
101
|
+
"""
|
|
102
|
+
Remove all downloaded files
|
|
103
|
+
"""
|
|
104
|
+
if not CamoufoxUpdate().cleanup():
|
|
105
|
+
rprint("Camoufox binaries not found!", fg="red")
|
|
106
|
+
# Remove the GeoIP database
|
|
107
|
+
remove_mmdb()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@cli.command(name='test')
|
|
111
|
+
@click.argument('url', default=None, required=False)
|
|
112
|
+
def test(url: Optional[str] = None) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Open the Playwright inspector
|
|
115
|
+
"""
|
|
116
|
+
from .sync_api import Camoufox
|
|
117
|
+
|
|
118
|
+
with Camoufox(headless=False, env=environ) as browser:
|
|
119
|
+
page = browser.new_page()
|
|
120
|
+
if url:
|
|
121
|
+
page.goto(url)
|
|
122
|
+
page.pause() # Open the Playwright inspector
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@cli.command(name='server')
|
|
126
|
+
def server() -> None:
|
|
127
|
+
"""
|
|
128
|
+
Launch a Playwright server
|
|
129
|
+
"""
|
|
130
|
+
from .server import launch_server
|
|
131
|
+
|
|
132
|
+
launch_server()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@cli.command(name='path')
|
|
136
|
+
def path() -> None:
|
|
137
|
+
"""
|
|
138
|
+
Display the path to the Camoufox executable
|
|
139
|
+
"""
|
|
140
|
+
rprint(INSTALL_DIR, fg="green")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@cli.command(name='version')
|
|
144
|
+
def version() -> None:
|
|
145
|
+
"""
|
|
146
|
+
Display the current version
|
|
147
|
+
"""
|
|
148
|
+
# python package version
|
|
149
|
+
try:
|
|
150
|
+
rprint(f"Pip package:\tv{pkg_version('camoufox')}", fg="green")
|
|
151
|
+
except PackageNotFoundError:
|
|
152
|
+
rprint("Pip package:\tNot installed!", fg="red")
|
|
153
|
+
|
|
154
|
+
updater = CamoufoxUpdate()
|
|
155
|
+
bin_ver = updater.current_verstr
|
|
156
|
+
|
|
157
|
+
# If binaries are not downloaded
|
|
158
|
+
if not bin_ver:
|
|
159
|
+
rprint("Camoufox:\tNot downloaded!", fg="red")
|
|
160
|
+
return
|
|
161
|
+
# Print the base version
|
|
162
|
+
rprint(f"Camoufox:\tv{bin_ver} ", fg="green", nl=False)
|
|
163
|
+
|
|
164
|
+
# Check for Camoufox updates
|
|
165
|
+
if updater.is_updated_needed():
|
|
166
|
+
rprint(f"(Latest supported: v{updater.verstr})", fg="red")
|
|
167
|
+
else:
|
|
168
|
+
rprint("(Up to date!)", fg="yellow")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == '__main__':
|
|
172
|
+
cli()
|
camoufox/__version__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Camoufox version constants.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CONSTRAINTS:
|
|
7
|
+
"""
|
|
8
|
+
The minimum and maximum supported versions of the Camoufox browser.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
MIN_VERSION = 'beta.12'
|
|
12
|
+
MAX_VERSION = '1'
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def as_range() -> str:
|
|
16
|
+
"""
|
|
17
|
+
Returns the version range as a string.
|
|
18
|
+
"""
|
|
19
|
+
return f">={CONSTRAINTS.MIN_VERSION}, <{CONSTRAINTS.MAX_VERSION}"
|
camoufox/addons.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import socket
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
import orjson
|
|
10
|
+
|
|
11
|
+
from .exceptions import InvalidAddonPath, InvalidDebugPort, MissingDebugPort
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DefaultAddons(Enum):
|
|
15
|
+
"""
|
|
16
|
+
Default addons to be downloaded
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
UBO = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi"
|
|
20
|
+
# Disable by default. Not always necessary, and increases the memory footprint of Camoufox.
|
|
21
|
+
# BPC = "https://gitflic.ru/project/magnolia1234/bpc_uploads/blob/raw?file=bypass_paywalls_clean-latest.xpi"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_debug_port(args: List[str]) -> int:
|
|
25
|
+
"""
|
|
26
|
+
Gets the debug port from the args, or creates a new one if not provided
|
|
27
|
+
"""
|
|
28
|
+
for i, arg in enumerate(args):
|
|
29
|
+
# Search for debugger server port
|
|
30
|
+
if arg == "-start-debugger-server":
|
|
31
|
+
# If arg is found but no port is provided, raise an error
|
|
32
|
+
if i + 1 >= len(args):
|
|
33
|
+
raise MissingDebugPort(f"No debug port provided: {args}")
|
|
34
|
+
debug_port = args[i + 1]
|
|
35
|
+
# Try to parse the debug port as an integer
|
|
36
|
+
try:
|
|
37
|
+
return int(debug_port)
|
|
38
|
+
except ValueError as e:
|
|
39
|
+
raise InvalidDebugPort(
|
|
40
|
+
f"Error parsing debug port. Must be an integer: {debug_port}"
|
|
41
|
+
) from e
|
|
42
|
+
|
|
43
|
+
# Create new debugger server port
|
|
44
|
+
debug_port_int = get_open_port()
|
|
45
|
+
# Add -start-debugger-server {debug_port} to args
|
|
46
|
+
args.extend(["-start-debugger-server", str(debug_port_int)])
|
|
47
|
+
|
|
48
|
+
return debug_port_int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def confirm_paths(paths: List[str]) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Confirms that the addon paths are valid
|
|
54
|
+
"""
|
|
55
|
+
for path in paths:
|
|
56
|
+
if not os.path.isdir(path):
|
|
57
|
+
raise InvalidAddonPath(path)
|
|
58
|
+
if not os.path.exists(os.path.join(path, 'manifest.json')):
|
|
59
|
+
raise InvalidAddonPath(
|
|
60
|
+
'manifest.json is missing. Addon path must be a path to an extracted addon.'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_open_port() -> int:
|
|
65
|
+
"""
|
|
66
|
+
Gets an open port
|
|
67
|
+
"""
|
|
68
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
69
|
+
s.bind(('localhost', 0))
|
|
70
|
+
return s.getsockname()[1]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def threaded_try_load_addons(debug_port_int: int, addons_list: List[str]) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Tries to load addons (in a separate thread)
|
|
76
|
+
"""
|
|
77
|
+
thread = threading.Thread(
|
|
78
|
+
target=try_load_addons, args=(debug_port_int, addons_list), daemon=True
|
|
79
|
+
)
|
|
80
|
+
thread.start()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def try_load_addons(debug_port_int: int, addons_list: List[str]) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Tries to load addons
|
|
86
|
+
"""
|
|
87
|
+
# Wait for the server to be open
|
|
88
|
+
while True:
|
|
89
|
+
try:
|
|
90
|
+
with socket.create_connection(("localhost", debug_port_int)):
|
|
91
|
+
break
|
|
92
|
+
except ConnectionRefusedError:
|
|
93
|
+
time.sleep(0.05)
|
|
94
|
+
|
|
95
|
+
# Load addons
|
|
96
|
+
asyncio.run(load_all_addons(debug_port_int, addons_list))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def load_all_addons(debug_port_int: int, addons_list: List[str]) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Loads all addons
|
|
102
|
+
"""
|
|
103
|
+
addon_loaders = [LoadFirefoxAddon(debug_port_int, addon) for addon in addons_list]
|
|
104
|
+
await asyncio.gather(*[addon_loader.load() for addon_loader in addon_loaders])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class LoadFirefoxAddon:
|
|
108
|
+
'''
|
|
109
|
+
Firefox addon loader
|
|
110
|
+
https://github.com/daijro/hrequests/blob/main/hrequests/extensions.py#L95
|
|
111
|
+
'''
|
|
112
|
+
|
|
113
|
+
def __init__(self, port, addon_path):
|
|
114
|
+
self.port: int = port
|
|
115
|
+
self.addon_path: str = addon_path
|
|
116
|
+
self.success: bool = False
|
|
117
|
+
self.buffers: list = []
|
|
118
|
+
self.remaining_bytes: int = 0
|
|
119
|
+
|
|
120
|
+
async def load(self):
|
|
121
|
+
reader, writer = await asyncio.open_connection('localhost', self.port)
|
|
122
|
+
writer.write(self._format_message({"to": "root", "type": "getRoot"}))
|
|
123
|
+
await writer.drain()
|
|
124
|
+
|
|
125
|
+
while True:
|
|
126
|
+
data = await reader.read(100) # Adjust buffer size as needed
|
|
127
|
+
if not data:
|
|
128
|
+
break
|
|
129
|
+
await self._process_data(writer, data)
|
|
130
|
+
|
|
131
|
+
writer.close()
|
|
132
|
+
await writer.wait_closed()
|
|
133
|
+
return self.success
|
|
134
|
+
|
|
135
|
+
async def _process_data(self, writer, data):
|
|
136
|
+
while data:
|
|
137
|
+
if self.remaining_bytes == 0:
|
|
138
|
+
index = data.find(b':')
|
|
139
|
+
if index == -1:
|
|
140
|
+
self.buffers.append(data)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
total_data = b''.join(self.buffers) + data
|
|
144
|
+
size, _, remainder = total_data.partition(b':')
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
self.remaining_bytes = int(size)
|
|
148
|
+
except ValueError as e:
|
|
149
|
+
raise ValueError("Invalid state") from e
|
|
150
|
+
|
|
151
|
+
data = remainder
|
|
152
|
+
|
|
153
|
+
if len(data) < self.remaining_bytes:
|
|
154
|
+
self.remaining_bytes -= len(data)
|
|
155
|
+
self.buffers.append(data)
|
|
156
|
+
return
|
|
157
|
+
else:
|
|
158
|
+
self.buffers.append(data[: self.remaining_bytes])
|
|
159
|
+
message = orjson.loads(b''.join(self.buffers))
|
|
160
|
+
self.buffers.clear()
|
|
161
|
+
|
|
162
|
+
await self._on_message(writer, message)
|
|
163
|
+
|
|
164
|
+
data = data[self.remaining_bytes :]
|
|
165
|
+
self.remaining_bytes = 0
|
|
166
|
+
|
|
167
|
+
async def _on_message(self, writer, message):
|
|
168
|
+
if "addonsActor" in message:
|
|
169
|
+
writer.write(
|
|
170
|
+
self._format_message(
|
|
171
|
+
{
|
|
172
|
+
"to": message["addonsActor"],
|
|
173
|
+
"type": "installTemporaryAddon",
|
|
174
|
+
"addonPath": self.addon_path,
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
await writer.drain()
|
|
179
|
+
|
|
180
|
+
if "addon" in message:
|
|
181
|
+
self.success = True
|
|
182
|
+
writer.write_eof()
|
|
183
|
+
|
|
184
|
+
if "error" in message:
|
|
185
|
+
writer.write_eof()
|
|
186
|
+
|
|
187
|
+
def _format_message(self, data):
|
|
188
|
+
raw = orjson.dumps(data)
|
|
189
|
+
return f"{len(raw)}:".encode() + raw
|
camoufox/async_api.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional, Union, overload
|
|
2
|
+
|
|
3
|
+
from playwright.async_api import (
|
|
4
|
+
Browser,
|
|
5
|
+
BrowserContext,
|
|
6
|
+
Playwright,
|
|
7
|
+
PlaywrightContextManager,
|
|
8
|
+
)
|
|
9
|
+
from typing_extensions import Literal
|
|
10
|
+
|
|
11
|
+
from camoufox.virtdisplay import VirtualDisplay
|
|
12
|
+
|
|
13
|
+
from .utils import async_attach_vd, launch_options
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncCamoufox(PlaywrightContextManager):
|
|
17
|
+
"""
|
|
18
|
+
Wrapper around playwright.async_api.PlaywrightContextManager that automatically
|
|
19
|
+
launches a browser and closes it when the context manager is exited.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, **launch_options):
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.launch_options = launch_options
|
|
25
|
+
self.browser: Optional[Union[Browser, BrowserContext]] = None
|
|
26
|
+
|
|
27
|
+
async def __aenter__(self) -> Union[Browser, BrowserContext]:
|
|
28
|
+
_playwright = await super().__aenter__()
|
|
29
|
+
self.browser = await AsyncNewBrowser(_playwright, **self.launch_options)
|
|
30
|
+
return self.browser
|
|
31
|
+
|
|
32
|
+
async def __aexit__(self, *args: Any):
|
|
33
|
+
if self.browser:
|
|
34
|
+
await self.browser.close()
|
|
35
|
+
await super().__aexit__(*args)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@overload
|
|
39
|
+
async def AsyncNewBrowser(
|
|
40
|
+
playwright: Playwright,
|
|
41
|
+
*,
|
|
42
|
+
from_options: Optional[Dict[str, Any]] = None,
|
|
43
|
+
persistent_context: Literal[False] = False,
|
|
44
|
+
**kwargs,
|
|
45
|
+
) -> Browser: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@overload
|
|
49
|
+
async def AsyncNewBrowser(
|
|
50
|
+
playwright: Playwright,
|
|
51
|
+
*,
|
|
52
|
+
from_options: Optional[Dict[str, Any]] = None,
|
|
53
|
+
persistent_context: Literal[True],
|
|
54
|
+
**kwargs,
|
|
55
|
+
) -> BrowserContext: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def AsyncNewBrowser(
|
|
59
|
+
playwright: Playwright,
|
|
60
|
+
*,
|
|
61
|
+
headless: Optional[Union[bool, Literal['virtual']]] = None,
|
|
62
|
+
from_options: Optional[Dict[str, Any]] = None,
|
|
63
|
+
persistent_context: bool = False,
|
|
64
|
+
debug: Optional[bool] = None,
|
|
65
|
+
**kwargs,
|
|
66
|
+
) -> Union[Browser, BrowserContext]:
|
|
67
|
+
"""
|
|
68
|
+
Launches a new browser instance for Camoufox given a set of launch options.
|
|
69
|
+
|
|
70
|
+
Parameters:
|
|
71
|
+
from_options (Dict[str, Any]):
|
|
72
|
+
A set of launch options generated by `launch_options()` to use
|
|
73
|
+
persistent_context (bool):
|
|
74
|
+
Whether to use a persistent context.
|
|
75
|
+
**kwargs:
|
|
76
|
+
All other keyword arugments passed to `launch_options()`.
|
|
77
|
+
"""
|
|
78
|
+
if headless == 'virtual':
|
|
79
|
+
virtual_display = VirtualDisplay(debug=debug)
|
|
80
|
+
kwargs['virtual_display'] = virtual_display.get()
|
|
81
|
+
headless = False
|
|
82
|
+
else:
|
|
83
|
+
virtual_display = None
|
|
84
|
+
|
|
85
|
+
opt = from_options or launch_options(headless=headless, debug=debug, **kwargs)
|
|
86
|
+
|
|
87
|
+
# Persistent context
|
|
88
|
+
if persistent_context:
|
|
89
|
+
context = await playwright.firefox.launch_persistent_context(**opt)
|
|
90
|
+
return await async_attach_vd(context, virtual_display)
|
|
91
|
+
|
|
92
|
+
# Browser
|
|
93
|
+
browser = await playwright.firefox.launch(**opt)
|
|
94
|
+
return await async_attach_vd(browser, virtual_display)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Mappings of Browserforge fingerprints to Camoufox config properties.
|
|
2
|
+
|
|
3
|
+
navigator:
|
|
4
|
+
# Note: Browserforge tends to have outdated UAs.
|
|
5
|
+
# The version will be replaced in Camoufox.
|
|
6
|
+
userAgent: navigator.userAgent
|
|
7
|
+
# userAgentData not in Firefox
|
|
8
|
+
doNotTrack: navigator.doNotTrack
|
|
9
|
+
appCodeName: navigator.appCodeName
|
|
10
|
+
appName: navigator.appName
|
|
11
|
+
appVersion: navigator.appVersion
|
|
12
|
+
oscpu: navigator.oscpu
|
|
13
|
+
# webdriver is always True
|
|
14
|
+
# Locale is now implemented separately:
|
|
15
|
+
# language: navigator.language
|
|
16
|
+
# languages: navigator.languages
|
|
17
|
+
platform: navigator.platform
|
|
18
|
+
# deviceMemory not in Firefox
|
|
19
|
+
hardwareConcurrency: navigator.hardwareConcurrency
|
|
20
|
+
product: navigator.product
|
|
21
|
+
productSub: navigator.productSub
|
|
22
|
+
# vendor is not necessary
|
|
23
|
+
# vendorSub is not necessary
|
|
24
|
+
maxTouchPoints: navigator.maxTouchPoints
|
|
25
|
+
extraProperties:
|
|
26
|
+
# Note: Changing pdfViewerEnabled is not recommended. This will be kept to True.
|
|
27
|
+
globalPrivacyControl: navigator.globalPrivacyControl
|
|
28
|
+
|
|
29
|
+
screen:
|
|
30
|
+
# hasHDR is not implemented in Camoufox
|
|
31
|
+
availLeft: screen.availLeft
|
|
32
|
+
availTop: screen.availTop
|
|
33
|
+
availWidth: screen.availWidth
|
|
34
|
+
availHeight: screen.availHeight
|
|
35
|
+
height: screen.height
|
|
36
|
+
width: screen.width
|
|
37
|
+
colorDepth: screen.colorDepth
|
|
38
|
+
pixelDepth: screen.pixelDepth
|
|
39
|
+
# devicePixelRatio is not recommended. Any value other than 1.0 is suspicious.
|
|
40
|
+
pageXOffset: screen.pageXOffset
|
|
41
|
+
pageYOffset: screen.pageYOffset
|
|
42
|
+
outerHeight: window.outerHeight
|
|
43
|
+
outerWidth: window.outerWidth
|
|
44
|
+
innerHeight: window.innerHeight
|
|
45
|
+
innerWidth: window.innerWidth
|
|
46
|
+
screenX: window.screenX
|
|
47
|
+
screenY: window.screenY
|
|
48
|
+
# Tends to generate out of bounds (network inconsistencies):
|
|
49
|
+
# clientWidth: document.body.clientWidth
|
|
50
|
+
# clientHeight: document.body.clientHeight
|
|
51
|
+
|
|
52
|
+
# videoCard:
|
|
53
|
+
# renderer: webgl:renderer
|
|
54
|
+
# vendor: webgl:vendor
|
|
55
|
+
|
|
56
|
+
headers:
|
|
57
|
+
# headers.User-Agent is redundant with navigator.userAgent
|
|
58
|
+
# headers.Accept-Language is redundant with locale:*
|
|
59
|
+
Accept-Encoding: headers.Accept-Encoding
|
|
60
|
+
|
|
61
|
+
battery:
|
|
62
|
+
charging: battery:charging
|
|
63
|
+
chargingTime: battery:chargingTime
|
|
64
|
+
dischargingTime: battery:dischargingTime
|
|
65
|
+
|
|
66
|
+
# Unsupported: videoCodecs, audioCodecs, pluginsData, multimediaDevices
|
|
67
|
+
# Fonts are listed through the launcher.
|