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.
- camoufox-0.1.0b0/PKG-INFO +156 -0
- camoufox-0.1.0b0/README.md +122 -0
- camoufox-0.1.0b0/camoufox/__init__.py +5 -0
- camoufox-0.1.0b0/camoufox/__main__.py +112 -0
- camoufox-0.1.0b0/camoufox/addons.py +184 -0
- camoufox-0.1.0b0/camoufox/async_api.py +88 -0
- camoufox-0.1.0b0/camoufox/browserforge.yaml +65 -0
- camoufox-0.1.0b0/camoufox/exceptions.py +54 -0
- camoufox-0.1.0b0/camoufox/fingerprints.py +50 -0
- camoufox-0.1.0b0/camoufox/fonts.json +11 -0
- camoufox-0.1.0b0/camoufox/pkgman.py +338 -0
- camoufox-0.1.0b0/camoufox/setup.cfg +3 -0
- camoufox-0.1.0b0/camoufox/sync_api.py +88 -0
- camoufox-0.1.0b0/camoufox/utils.py +221 -0
- camoufox-0.1.0b0/camoufox/xpi_dl.py +61 -0
- camoufox-0.1.0b0/pyproject.toml +43 -0
|
@@ -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,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)
|