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 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()
@@ -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.