camoufox 0.1.0b0__tar.gz

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.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.1
2
+ Name: camoufox
3
+ Version: 0.1.0b0
4
+ Summary: Wrapper around Playwright to help launch Camoufox
5
+ Home-page: https://github.com/daijro/camoufox
6
+ License: MIT
7
+ Keywords: client,fingerprint,browser,scraping,injector,firefox,playwright
8
+ Author: daijro
9
+ Author-email: daijro.dev@gmail.com
10
+ Requires-Python: >=3.8,<4.0
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Dist: browserforge
22
+ Requires-Dist: click
23
+ Requires-Dist: numpy
24
+ Requires-Dist: orjson
25
+ Requires-Dist: platformdirs
26
+ Requires-Dist: playwright
27
+ Requires-Dist: pyyaml
28
+ Requires-Dist: requests
29
+ Requires-Dist: tqdm
30
+ Requires-Dist: typing_extensions
31
+ Requires-Dist: ua_parser
32
+ Project-URL: Repository, https://github.com/daijro/camoufox
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Camoufox Python Interface
36
+
37
+ #### This is the Python library for Camoufox.
38
+
39
+ > [!WARNING]
40
+ > This is still experimental and in active development!
41
+
42
+ ## Installation
43
+
44
+ First, install the `camoufox` package:
45
+
46
+ ```bash
47
+ pip install -U camoufox
48
+ ```
49
+
50
+ Then, download the Camoufox browser:
51
+
52
+ ```bash
53
+ camoufox fetch
54
+ ```
55
+
56
+ To uninstall, run `camoufox remove`.
57
+
58
+ <hr width=50>
59
+
60
+ ## Usage
61
+
62
+ Camoufox is fully compatible with your existing Playwright code. You only have to change your browser initialization:
63
+
64
+ #### Sync API
65
+
66
+ ```python
67
+ from camoufox.sync_api import Camoufox
68
+
69
+ with Camoufox(headless=False) as browser:
70
+ page = browser.new_page()
71
+ page.goto("https://example.com/")
72
+ ```
73
+
74
+ #### Async API
75
+
76
+ ```python
77
+ from camoufox.async_api import AsyncCamoufox
78
+
79
+ async with AsyncCamoufox(headless=False) as browser:
80
+ page = await browser.new_page()
81
+ await page.goto("https://example.com")
82
+ ```
83
+ <details>
84
+ <summary>Parameters</summary>
85
+
86
+ ```
87
+ Launches a new browser instance for Camoufox.
88
+
89
+ Parameters:
90
+ playwright (Playwright):
91
+ The playwright instance to use.
92
+ config (Optional[Dict[str, Any]]):
93
+ The configuration to use.
94
+ addons (Optional[List[str]]):
95
+ The addons to use.
96
+ fingerprint (Optional[Fingerprint]):
97
+ The fingerprint to use.
98
+ exclude_addons (Optional[List[DefaultAddons]]):
99
+ The default addons to exclude, passed as a list of camoufox.DefaultAddons enums.
100
+ screen (Optional[browserforge.fingerprints.Screen]):
101
+ The screen constraints to use.
102
+ os (Optional[ListOrString]):
103
+ The operating system to use for the fingerprint. Either a string or a list of strings.
104
+ user_agent (Optional[ListOrString]):
105
+ The user agent to use for the fingerprint. Either a string or a list of strings.
106
+ fonts (Optional[List[str]]):
107
+ The fonts to load into Camoufox, in addition to the default fonts.
108
+ args (Optional[List[str]]):
109
+ The arguments to pass to the browser.
110
+ executable_path (Optional[str]):
111
+ The path to the Camoufox browser executable.
112
+ **launch_options (Dict[str, Any]):
113
+ Additional Firefox launch options.
114
+ ```
115
+ </details>
116
+
117
+ ---
118
+
119
+ ### Config
120
+
121
+ Camoufox [config data](https://github.com/daijro/camoufox?tab=readme-ov-file#fingerprint-injection) can be passed as a dictionary to the `config` parameter:
122
+
123
+ ```python
124
+ from camoufox import Camoufox
125
+
126
+ with Camoufox(
127
+ config={
128
+ 'webrtc:ipv4': '123.45.67.89',
129
+ 'webrtc:ipv6': 'e791:d37a:88f6:48d1:2cad:2667:4582:1d6d',
130
+ }
131
+ ) as browser:
132
+ page = browser.new_page()
133
+ page.goto("https://www.browserscan.net/")
134
+ ```
135
+
136
+ <hr width=50>
137
+
138
+ ### BrowserForge Integration
139
+
140
+ Camoufox is fully compatible with BrowserForge.
141
+
142
+ By default, Camoufox will use a random fingerprint. You can also inject your own Firefox Browserforge fingerprint into Camoufox with the following example:
143
+
144
+ ```python
145
+ from camoufox.sync_api import Camoufox
146
+ from browserforge.fingerprints import FingerprintGenerator
147
+
148
+ fg = FingerprintGenerator(browser='firefox')
149
+
150
+ # Launch Camoufox with a random Firefox fingerprint
151
+ with Camoufox(fingerprint=fg.generate()) as browser:
152
+ page = browser.new_page()
153
+ page.goto("https://www.browserscan.net/")
154
+ ```
155
+
156
+ ---
@@ -0,0 +1,122 @@
1
+ # Camoufox Python Interface
2
+
3
+ #### This is the Python library for Camoufox.
4
+
5
+ > [!WARNING]
6
+ > This is still experimental and in active development!
7
+
8
+ ## Installation
9
+
10
+ First, install the `camoufox` package:
11
+
12
+ ```bash
13
+ pip install -U camoufox
14
+ ```
15
+
16
+ Then, download the Camoufox browser:
17
+
18
+ ```bash
19
+ camoufox fetch
20
+ ```
21
+
22
+ To uninstall, run `camoufox remove`.
23
+
24
+ <hr width=50>
25
+
26
+ ## Usage
27
+
28
+ Camoufox is fully compatible with your existing Playwright code. You only have to change your browser initialization:
29
+
30
+ #### Sync API
31
+
32
+ ```python
33
+ from camoufox.sync_api import Camoufox
34
+
35
+ with Camoufox(headless=False) as browser:
36
+ page = browser.new_page()
37
+ page.goto("https://example.com/")
38
+ ```
39
+
40
+ #### Async API
41
+
42
+ ```python
43
+ from camoufox.async_api import AsyncCamoufox
44
+
45
+ async with AsyncCamoufox(headless=False) as browser:
46
+ page = await browser.new_page()
47
+ await page.goto("https://example.com")
48
+ ```
49
+ <details>
50
+ <summary>Parameters</summary>
51
+
52
+ ```
53
+ Launches a new browser instance for Camoufox.
54
+
55
+ Parameters:
56
+ playwright (Playwright):
57
+ The playwright instance to use.
58
+ config (Optional[Dict[str, Any]]):
59
+ The configuration to use.
60
+ addons (Optional[List[str]]):
61
+ The addons to use.
62
+ fingerprint (Optional[Fingerprint]):
63
+ The fingerprint to use.
64
+ exclude_addons (Optional[List[DefaultAddons]]):
65
+ The default addons to exclude, passed as a list of camoufox.DefaultAddons enums.
66
+ screen (Optional[browserforge.fingerprints.Screen]):
67
+ The screen constraints to use.
68
+ os (Optional[ListOrString]):
69
+ The operating system to use for the fingerprint. Either a string or a list of strings.
70
+ user_agent (Optional[ListOrString]):
71
+ The user agent to use for the fingerprint. Either a string or a list of strings.
72
+ fonts (Optional[List[str]]):
73
+ The fonts to load into Camoufox, in addition to the default fonts.
74
+ args (Optional[List[str]]):
75
+ The arguments to pass to the browser.
76
+ executable_path (Optional[str]):
77
+ The path to the Camoufox browser executable.
78
+ **launch_options (Dict[str, Any]):
79
+ Additional Firefox launch options.
80
+ ```
81
+ </details>
82
+
83
+ ---
84
+
85
+ ### Config
86
+
87
+ Camoufox [config data](https://github.com/daijro/camoufox?tab=readme-ov-file#fingerprint-injection) can be passed as a dictionary to the `config` parameter:
88
+
89
+ ```python
90
+ from camoufox import Camoufox
91
+
92
+ with Camoufox(
93
+ config={
94
+ 'webrtc:ipv4': '123.45.67.89',
95
+ 'webrtc:ipv6': 'e791:d37a:88f6:48d1:2cad:2667:4582:1d6d',
96
+ }
97
+ ) as browser:
98
+ page = browser.new_page()
99
+ page.goto("https://www.browserscan.net/")
100
+ ```
101
+
102
+ <hr width=50>
103
+
104
+ ### BrowserForge Integration
105
+
106
+ Camoufox is fully compatible with BrowserForge.
107
+
108
+ By default, Camoufox will use a random fingerprint. You can also inject your own Firefox Browserforge fingerprint into Camoufox with the following example:
109
+
110
+ ```python
111
+ from camoufox.sync_api import Camoufox
112
+ from browserforge.fingerprints import FingerprintGenerator
113
+
114
+ fg = FingerprintGenerator(browser='firefox')
115
+
116
+ # Launch Camoufox with a random Firefox fingerprint
117
+ with Camoufox(fingerprint=fg.generate()) as browser:
118
+ page = browser.new_page()
119
+ page.goto("https://www.browserscan.net/")
120
+ ```
121
+
122
+ ---
@@ -0,0 +1,5 @@
1
+ from .addons import DefaultAddons
2
+ from .async_api import AsyncCamoufox, AsyncNewBrowser
3
+ from .sync_api import Camoufox, NewBrowser
4
+
5
+ __all__ = ["Camoufox", "NewBrowser", "AsyncCamoufox", "AsyncNewBrowser", "DefaultAddons"]
@@ -0,0 +1,112 @@
1
+ """
2
+ Binary CLI 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
+
10
+ import click
11
+
12
+ from .pkgman import CamoufoxFetcher, installed_verstr, rprint
13
+
14
+
15
+ class CamoufoxUpdate(CamoufoxFetcher):
16
+ """
17
+ Checks & updates Camoufox
18
+ """
19
+
20
+ def __init__(self) -> None:
21
+ """
22
+ Initializes the CamoufoxUpdate class
23
+ """
24
+ super().__init__()
25
+ try:
26
+ self.current_verstr = installed_verstr()
27
+ except FileNotFoundError:
28
+ self.current_verstr = None
29
+
30
+ def is_updated_needed(self) -> None:
31
+ if self.current_verstr is None:
32
+ return True
33
+ # If the installed version is not the latest version
34
+ if self.current_verstr != self.verstr:
35
+ return True
36
+ return False
37
+
38
+ def update(self) -> None:
39
+ """
40
+ Updates the library if needed
41
+ """
42
+ # Check if the version is the same as the latest available version
43
+ if not self.is_updated_needed():
44
+ rprint("Camoufox binaries up to date!", fg="green")
45
+ rprint(f"Current version: v{self.current_verstr}", fg="green")
46
+ return
47
+
48
+ # Download updated file
49
+ if self.current_verstr is not None:
50
+ # Display an updating message
51
+ rprint(
52
+ f"Updating Camoufox binaries from v{self.current_verstr} => v{self.verstr}",
53
+ fg="yellow",
54
+ )
55
+ else:
56
+ rprint(f"Fetching Camoufox binaries v{self.verstr}...", fg="yellow")
57
+ # Install the new version
58
+ self.install()
59
+
60
+
61
+ @click.group()
62
+ def cli() -> None:
63
+ pass
64
+
65
+
66
+ @cli.command(name='fetch')
67
+ def fetch() -> None:
68
+ """
69
+ Fetch the latest version of Camoufox
70
+ """
71
+ CamoufoxUpdate().update()
72
+
73
+
74
+ @cli.command(name='remove')
75
+ def remove() -> None:
76
+ """
77
+ Remove all library files
78
+ """
79
+ if not CamoufoxUpdate().cleanup():
80
+ rprint("Camoufox binaries not found!", fg="red")
81
+
82
+
83
+ @cli.command(name='version')
84
+ def version() -> None:
85
+ """
86
+ Display the current version
87
+ """
88
+ # python package version
89
+ try:
90
+ rprint(f"Pip package:\tv{pkg_version('camoufox')}", fg="green")
91
+ except PackageNotFoundError:
92
+ rprint("Pip package:\tNot installed!", fg="red")
93
+
94
+ updater = CamoufoxUpdate()
95
+ bin_ver = updater.current_verstr
96
+
97
+ # If binaries are not downloaded
98
+ if not bin_ver:
99
+ rprint("Camoufox:\tNot downloaded!", fg="red")
100
+ return
101
+ # Print the base version
102
+ rprint(f"Camoufox:\tv{bin_ver} ", fg="green", nl=False)
103
+
104
+ # Check for library updates
105
+ if updater.is_updated_needed():
106
+ rprint(f"(Latest: v{updater.verstr})", fg="red")
107
+ else:
108
+ rprint("(Up to date!)", fg="yellow")
109
+
110
+
111
+ if __name__ == '__main__':
112
+ cli()
@@ -0,0 +1,184 @@
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
+ BPC = "https://gitflic.ru/project/magnolia1234/bpc_uploads/blob/raw?file=bypass_paywalls_clean-latest.xpi"
21
+
22
+
23
+ def get_debug_port(args: List[str]) -> int:
24
+ """
25
+ Gets the debug port from the args, or creates a new one if not provided
26
+ """
27
+ for i, arg in enumerate(args):
28
+ # Search for debugger server port
29
+ if arg == "-start-debugger-server":
30
+ # If arg is found but no port is provided, raise an error
31
+ if i + 1 >= len(args):
32
+ raise MissingDebugPort(f"No debug port provided: {args}")
33
+ debug_port = args[i + 1]
34
+ # Try to parse the debug port as an integer
35
+ try:
36
+ return int(debug_port)
37
+ except ValueError as e:
38
+ raise InvalidDebugPort(
39
+ f"Error parsing debug port. Must be an integer: {debug_port}"
40
+ ) from e
41
+
42
+ # Create new debugger server port
43
+ debug_port_int = get_open_port()
44
+ # Add -start-debugger-server {debug_port} to args
45
+ args.extend(["-start-debugger-server", str(debug_port_int)])
46
+
47
+ return debug_port_int
48
+
49
+
50
+ def confirm_paths(paths: List[str]) -> None:
51
+ """
52
+ Confirms that the addon paths are valid
53
+ """
54
+ for path in paths:
55
+ if not os.path.exists(path):
56
+ raise InvalidAddonPath(path)
57
+
58
+
59
+ def get_open_port() -> int:
60
+ """
61
+ Gets an open port
62
+ """
63
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
64
+ s.bind(('localhost', 0))
65
+ return s.getsockname()[1]
66
+
67
+
68
+ def threaded_try_load_addons(debug_port_int: int, addons_list: List[str]) -> None:
69
+ """
70
+ Tries to load addons (in a separate thread)
71
+ """
72
+ thread = threading.Thread(
73
+ target=try_load_addons, args=(debug_port_int, addons_list), daemon=True
74
+ )
75
+ thread.start()
76
+
77
+
78
+ def try_load_addons(debug_port_int: int, addons_list: List[str]) -> None:
79
+ """
80
+ Tries to load addons
81
+ """
82
+ # Wait for the server to be open
83
+ while True:
84
+ try:
85
+ with socket.create_connection(("localhost", debug_port_int)):
86
+ break
87
+ except ConnectionRefusedError:
88
+ time.sleep(0.05)
89
+
90
+ # Load addons
91
+ asyncio.run(load_all_addons(debug_port_int, addons_list))
92
+
93
+
94
+ async def load_all_addons(debug_port_int: int, addons_list: List[str]) -> None:
95
+ """
96
+ Loads all addons
97
+ """
98
+ addon_loaders = [LoadFirefoxAddon(debug_port_int, addon) for addon in addons_list]
99
+ await asyncio.gather(*[addon_loader.load() for addon_loader in addon_loaders])
100
+
101
+
102
+ class LoadFirefoxAddon:
103
+ '''
104
+ Firefox addon loader
105
+ https://github.com/daijro/hrequests/blob/main/hrequests/extensions.py#L95
106
+ '''
107
+
108
+ def __init__(self, port, addon_path):
109
+ self.port: int = port
110
+ self.addon_path: str = addon_path
111
+ self.success: bool = False
112
+ self.buffers: list = []
113
+ self.remaining_bytes: int = 0
114
+
115
+ async def load(self):
116
+ reader, writer = await asyncio.open_connection('localhost', self.port)
117
+ writer.write(self._format_message({"to": "root", "type": "getRoot"}))
118
+ await writer.drain()
119
+
120
+ while True:
121
+ data = await reader.read(100) # Adjust buffer size as needed
122
+ if not data:
123
+ break
124
+ await self._process_data(writer, data)
125
+
126
+ writer.close()
127
+ await writer.wait_closed()
128
+ return self.success
129
+
130
+ async def _process_data(self, writer, data):
131
+ while data:
132
+ if self.remaining_bytes == 0:
133
+ index = data.find(b':')
134
+ if index == -1:
135
+ self.buffers.append(data)
136
+ return
137
+
138
+ total_data = b''.join(self.buffers) + data
139
+ size, _, remainder = total_data.partition(b':')
140
+
141
+ try:
142
+ self.remaining_bytes = int(size)
143
+ except ValueError as e:
144
+ raise ValueError("Invalid state") from e
145
+
146
+ data = remainder
147
+
148
+ if len(data) < self.remaining_bytes:
149
+ self.remaining_bytes -= len(data)
150
+ self.buffers.append(data)
151
+ return
152
+ else:
153
+ self.buffers.append(data[: self.remaining_bytes])
154
+ message = orjson.loads(b''.join(self.buffers))
155
+ self.buffers.clear()
156
+
157
+ await self._on_message(writer, message)
158
+
159
+ data = data[self.remaining_bytes :]
160
+ self.remaining_bytes = 0
161
+
162
+ async def _on_message(self, writer, message):
163
+ if "addonsActor" in message:
164
+ writer.write(
165
+ self._format_message(
166
+ {
167
+ "to": message["addonsActor"],
168
+ "type": "installTemporaryAddon",
169
+ "addonPath": self.addon_path,
170
+ }
171
+ )
172
+ )
173
+ await writer.drain()
174
+
175
+ if "addon" in message:
176
+ self.success = True
177
+ writer.write_eof()
178
+
179
+ if "error" in message:
180
+ writer.write_eof()
181
+
182
+ def _format_message(self, data):
183
+ raw = orjson.dumps(data)
184
+ return f"{len(raw)}:".encode() + raw
@@ -0,0 +1,88 @@
1
+ from typing import Any, Dict, List, Optional
2
+
3
+ from browserforge.fingerprints import Fingerprint, Screen
4
+ from playwright.async_api import Browser, Playwright, PlaywrightContextManager
5
+
6
+ from .addons import DefaultAddons
7
+ from .utils import ListOrString, get_launch_options
8
+
9
+
10
+ class AsyncCamoufox(PlaywrightContextManager):
11
+ """
12
+ Wrapper around playwright.async_api.PlaywrightContextManager that automatically
13
+ launches a browser and closes it when the context manager is exited.
14
+ """
15
+
16
+ def __init__(self, **launch_options):
17
+ super().__init__()
18
+ self.launch_options = launch_options
19
+ self.browser: Optional[Browser] = None
20
+
21
+ async def __aenter__(self) -> Browser:
22
+ await super().__aenter__()
23
+ self.browser = await AsyncNewBrowser(self._playwright, **self.launch_options)
24
+ return self.browser
25
+
26
+ async def __aexit__(self, *args: Any):
27
+ if self.browser:
28
+ await self.browser.close()
29
+ await super().__aexit__(*args)
30
+
31
+
32
+ async def AsyncNewBrowser(
33
+ playwright: Playwright,
34
+ *,
35
+ config: Optional[Dict[str, Any]] = None,
36
+ addons: Optional[List[str]] = None,
37
+ fingerprint: Optional[Fingerprint] = None,
38
+ exclude_addons: Optional[List[DefaultAddons]] = None,
39
+ screen: Optional[Screen] = None,
40
+ os: Optional[ListOrString] = None,
41
+ user_agent: Optional[ListOrString] = None,
42
+ fonts: Optional[List[str]] = None,
43
+ args: Optional[List[str]] = None,
44
+ executable_path: Optional[str] = None,
45
+ **launch_options: Dict[str, Any]
46
+ ) -> Browser:
47
+ """
48
+ Launches a new browser instance for Camoufox.
49
+
50
+ Parameters:
51
+ playwright (Playwright):
52
+ The playwright instance to use.
53
+ config (Optional[Dict[str, Any]]):
54
+ The configuration to use.
55
+ addons (Optional[List[str]]):
56
+ The addons to use.
57
+ fingerprint (Optional[Fingerprint]):
58
+ The fingerprint to use.
59
+ exclude_addons (Optional[List[DefaultAddons]]):
60
+ The default addons to exclude, passed as a list of camoufox.DefaultAddons enums.
61
+ screen (Optional[browserforge.fingerprints.Screen]):
62
+ The screen constraints to use.
63
+ os (Optional[ListOrString]):
64
+ The operating system to use for the fingerprint. Either a string or a list of strings.
65
+ user_agent (Optional[ListOrString]):
66
+ The user agent to use for the fingerprint. Either a string or a list of strings.
67
+ fonts (Optional[List[str]]):
68
+ The fonts to load into Camoufox, in addition to the default fonts.
69
+ args (Optional[List[str]]):
70
+ The arguments to pass to the browser.
71
+ executable_path (Optional[str]):
72
+ The path to the Camoufox browser executable.
73
+ **launch_options (Dict[str, Any]):
74
+ Additional Firefox launch options.
75
+ """
76
+ opt = get_launch_options(
77
+ config=config,
78
+ addons=addons,
79
+ fingerprint=fingerprint,
80
+ exclude_addons=exclude_addons,
81
+ screen=screen,
82
+ os=os,
83
+ user_agent=user_agent,
84
+ fonts=fonts,
85
+ args=args,
86
+ executable_path=executable_path,
87
+ )
88
+ return await playwright.firefox.launch(**opt, **launch_options)