invisible-playwright 0.3.5__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.
- invisible_playwright-0.3.5/.gitignore +16 -0
- invisible_playwright-0.3.5/LICENSE +21 -0
- invisible_playwright-0.3.5/PKG-INFO +234 -0
- invisible_playwright-0.3.5/README.md +198 -0
- invisible_playwright-0.3.5/pyproject.toml +106 -0
- invisible_playwright-0.3.5/src/invisible_playwright/__init__.py +68 -0
- invisible_playwright-0.3.5/src/invisible_playwright/__main__.py +4 -0
- invisible_playwright-0.3.5/src/invisible_playwright/_engine.py +90 -0
- invisible_playwright-0.3.5/src/invisible_playwright/_fpforge/__init__.py +17 -0
- invisible_playwright-0.3.5/src/invisible_playwright/_geo.py +4 -0
- invisible_playwright-0.3.5/src/invisible_playwright/_headless.py +4 -0
- invisible_playwright-0.3.5/src/invisible_playwright/_pin.py +143 -0
- invisible_playwright-0.3.5/src/invisible_playwright/_proxy.py +4 -0
- invisible_playwright-0.3.5/src/invisible_playwright/_recaptcha_seed.py +340 -0
- invisible_playwright-0.3.5/src/invisible_playwright/_webgl_personas.py +4 -0
- invisible_playwright-0.3.5/src/invisible_playwright/async_api.py +271 -0
- invisible_playwright-0.3.5/src/invisible_playwright/cli.py +127 -0
- invisible_playwright-0.3.5/src/invisible_playwright/config.py +4 -0
- invisible_playwright-0.3.5/src/invisible_playwright/constants.py +4 -0
- invisible_playwright-0.3.5/src/invisible_playwright/download.py +4 -0
- invisible_playwright-0.3.5/src/invisible_playwright/launcher.py +436 -0
- invisible_playwright-0.3.5/src/invisible_playwright/prefs.py +4 -0
- invisible_playwright-0.3.5/src/invisible_playwright/py.typed +0 -0
- invisible_playwright-0.3.5/src/invisible_playwright/sync_api.py +4 -0
- invisible_playwright-0.3.5/tests/conftest.py +54 -0
- invisible_playwright-0.3.5/tests/test_async_api.py +83 -0
- invisible_playwright-0.3.5/tests/test_backcompat.py +84 -0
- invisible_playwright-0.3.5/tests/test_build.py +42 -0
- invisible_playwright-0.3.5/tests/test_canvas_render_stealth.py +67 -0
- invisible_playwright-0.3.5/tests/test_cli.py +174 -0
- invisible_playwright-0.3.5/tests/test_cloak.py +115 -0
- invisible_playwright-0.3.5/tests/test_constants.py +217 -0
- invisible_playwright-0.3.5/tests/test_core_pin.py +417 -0
- invisible_playwright-0.3.5/tests/test_cross_origin_iframe.py +278 -0
- invisible_playwright-0.3.5/tests/test_detectors_e2e.py +294 -0
- invisible_playwright-0.3.5/tests/test_download.py +590 -0
- invisible_playwright-0.3.5/tests/test_e2e.py +219 -0
- invisible_playwright-0.3.5/tests/test_fingerprint_consistency.py +510 -0
- invisible_playwright-0.3.5/tests/test_fingerprint_surface.py +311 -0
- invisible_playwright-0.3.5/tests/test_fpforge.py +77 -0
- invisible_playwright-0.3.5/tests/test_geo.py +325 -0
- invisible_playwright-0.3.5/tests/test_geoip_update.py +142 -0
- invisible_playwright-0.3.5/tests/test_headless.py +124 -0
- invisible_playwright-0.3.5/tests/test_imports.py +56 -0
- invisible_playwright-0.3.5/tests/test_integration.py +358 -0
- invisible_playwright-0.3.5/tests/test_launcher_config.py +271 -0
- invisible_playwright-0.3.5/tests/test_launcher_helpers.py +229 -0
- invisible_playwright-0.3.5/tests/test_mouse.py +255 -0
- invisible_playwright-0.3.5/tests/test_network.py +260 -0
- invisible_playwright-0.3.5/tests/test_pin.py +72 -0
- invisible_playwright-0.3.5/tests/test_prefs.py +486 -0
- invisible_playwright-0.3.5/tests/test_profile.py +296 -0
- invisible_playwright-0.3.5/tests/test_proxy.py +266 -0
- invisible_playwright-0.3.5/tests/test_proxy_socks_auth_e2e.py +197 -0
- invisible_playwright-0.3.5/tests/test_recaptcha_seed.py +349 -0
- invisible_playwright-0.3.5/tests/test_release_e2e.py +269 -0
- invisible_playwright-0.3.5/tests/test_sampler.py +316 -0
- invisible_playwright-0.3.5/tests/test_seal_binary_path.py +177 -0
- invisible_playwright-0.3.5/tests/test_seal_floor.py +263 -0
- invisible_playwright-0.3.5/tests/test_seal_playwright_range.py +79 -0
- invisible_playwright-0.3.5/tests/test_seal_wire_version.py +72 -0
- invisible_playwright-0.3.5/tests/test_service_worker.py +225 -0
- invisible_playwright-0.3.5/tests/test_version.py +114 -0
- invisible_playwright-0.3.5/tests/test_webgl_noise_active.py +63 -0
- invisible_playwright-0.3.5/tests/test_webrtc_realness.py +490 -0
- invisible_playwright-0.3.5/tests/unit/test_config_public.py +125 -0
- invisible_playwright-0.3.5/tests/vendor/README.md +24 -0
- invisible_playwright-0.3.5/tests/vendor/botd-2.0.0.esm.js +811 -0
- invisible_playwright-0.3.5/tests/vendor/creepjs-10aa672.js +9710 -0
- invisible_playwright-0.3.5/tests/vendor/fingerprintjs-5.2.0.umd.min.js +27 -0
- invisible_playwright-0.3.5/tests/vendor/fpscanner-1.0.6.es.js +1253 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.venv/
|
|
8
|
+
firefox-source/
|
|
9
|
+
|
|
10
|
+
# Third-party source trees kept on disk for on-demand compatibility audits.
|
|
11
|
+
# They are somebody else's code, with their own licence and their own .git, and
|
|
12
|
+
# they must never be committed into this repository: `git add -A` would either
|
|
13
|
+
# stage 3.9 MB of Apache-2.0 sources into an MIT repo or, worse, record a bare
|
|
14
|
+
# gitlink that nobody can clone. The sdist/wheel excludes only protect the
|
|
15
|
+
# published artifacts, not the repo, so the ignore has to live here too.
|
|
16
|
+
tests/playwright-upstream/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stealthfox contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: invisible-playwright
|
|
3
|
+
Version: 0.3.5
|
|
4
|
+
Summary: Playwright wrapper for a patched Firefox with deterministic stealth profile.
|
|
5
|
+
Project-URL: Homepage, https://github.com/feder-cr/invisible_playwright
|
|
6
|
+
Project-URL: Repository, https://github.com/feder-cr/invisible_playwright
|
|
7
|
+
Project-URL: Issues, https://github.com/feder-cr/invisible_playwright/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/feder-cr/invisible_playwright/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Documentation, https://github.com/feder-cr/invisible_playwright#readme
|
|
10
|
+
Author-email: feder-cr <85809106+feder-cr@users.noreply.github.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: anti-detect,automation,firefox,playwright,stealth
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: invisible-core==18.0.0
|
|
27
|
+
Requires-Dist: playwright<=1.61.0,>=1.55
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build>=1; extra == 'dev'
|
|
30
|
+
Requires-Dist: playwright<=1.61.0,>=1.55; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-mock>=3; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-rerunfailures>=14; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
34
|
+
Requires-Dist: responses>=0.24; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
<p>
|
|
38
|
+
<a href="https://github.com/feder-cr/invisible_playwright/actions/workflows/tests.yml"><img src="https://github.com/feder-cr/invisible_playwright/actions/workflows/tests.yml/badge.svg" alt="tests"></a>
|
|
39
|
+
<a href="https://github.com/feder-cr/invisible_playwright/blob/main/LICENSE"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/license.svg" alt="License: MIT"></a>
|
|
40
|
+
<a href="https://www.python.org/downloads/"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/python.svg" alt="Python 3.11+"></a>
|
|
41
|
+
<a href="https://github.com/feder-cr/firefox_antidetect_patch/releases"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/firefox.svg" alt="Firefox 151.0"></a>
|
|
42
|
+
<a href="https://github.com/feder-cr/invisible_playwright/stargazers"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/stars.svg" alt="GitHub stars"></a>
|
|
43
|
+
<a href="https://github.com/feder-cr/firefox_antidetect_patch/releases/tag/usage-counter"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/launches.svg" alt="browser launches"></a>
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<div align="center">
|
|
47
|
+
<picture>
|
|
48
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/feder-cr/invisible_playwright/7a8693c6b4386e9a84dd93bedc479ca8654482e1/docs/banner-dark.png">
|
|
49
|
+
<img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/7a8693c6b4386e9a84dd93bedc479ca8654482e1/docs/banner-light.png" alt="invisible_playwright" width="620">
|
|
50
|
+
</picture>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<h3 align="center">A modified Firefox that passes every bot detection test.</h3>
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
## How it works
|
|
58
|
+
|
|
59
|
+
Anti-bots ask two questions. invisible_playwright answers yes to both.
|
|
60
|
+
|
|
61
|
+
**1. Is this a real browser?** Yes. It is Firefox, patched at the C++ source level.
|
|
62
|
+
|
|
63
|
+
- Fingerprint set inside the engine, not injected into the page: Navigator, screen, GPU/WebGL, Canvas, fonts, audio, WebRTC, timezone, network.
|
|
64
|
+
- No JS shim, no override, no seam to read.
|
|
65
|
+
|
|
66
|
+
**2. Is a real person using it?** Yes. The actions are humanized in the driver.
|
|
67
|
+
|
|
68
|
+
- Every click, hover and drag follows a natural mouse path with human timing, no teleporting cursor.
|
|
69
|
+
- Each input is byte-identical to a real mouse: real input source, pressure, trusted events.
|
|
70
|
+
|
|
71
|
+
Driven by the standard Playwright API. Full breakdown: [feder-cr/firefox_antidetect_patch](https://github.com/feder-cr/firefox_antidetect_patch).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Still seeing captchas or anti-bot? It's the proxy.
|
|
76
|
+
Once the browser is handled it stops being the variable. If you are still getting challenged, the tell is no longer the browser, it is the IP you come from. Around 90% of proxies are public: anyone can rent the same address, so it is already known and sits on the blocked-IP lists sites check. A perfect browser on a known IP still loses.
|
|
77
|
+
|
|
78
|
+
> The fix is the clean 10%, residential IPs that aren't already known. For those we recommend [sx.org](https://sx.org/?c=invisible_playwright), who filter for and serve only IPs that aren't already on those lists.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Install
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install invisible-playwright
|
|
86
|
+
python -m invisible_playwright fetch # one-time ~100 MB download, SHA256-verified
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Supported platforms: **Windows x86_64**, **Linux x86_64 / arm64**, **macOS arm64 / x86_64**. On macOS the app is ad-hoc signed (not notarized): if Gatekeeper complains, clear the quarantine flag once with `xattr -dr com.apple.quarantine` on the cached `Firefox.app`.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Usage
|
|
94
|
+
### Random fingerprint per session
|
|
95
|
+
**100% Playwright-compatible** - sync and async, all methods, zero API changes. If you already use Playwright, switching is two lines:
|
|
96
|
+
|
|
97
|
+
```diff
|
|
98
|
+
- from playwright.sync_api import sync_playwright
|
|
99
|
+
- with sync_playwright() as p:
|
|
100
|
+
- browser = p.firefox.launch()
|
|
101
|
+
+ from invisible_playwright import InvisiblePlaywright
|
|
102
|
+
+ with InvisiblePlaywright() as browser:
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Every session gets a distinct fingerprint (GPU, audio, fonts, screen, ~400 fields) and Bezier-curve mouse motion.
|
|
106
|
+
|
|
107
|
+
**Sync**
|
|
108
|
+
```python
|
|
109
|
+
from invisible_playwright import InvisiblePlaywright
|
|
110
|
+
|
|
111
|
+
with InvisiblePlaywright(proxy={"server": "socks5://...", "username": "u", "password": "p"}) as browser:
|
|
112
|
+
page = browser.new_page()
|
|
113
|
+
page.goto("https://example.com")
|
|
114
|
+
page.click("#submit") # mouse arcs to the button on a Bezier curve
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Async**
|
|
118
|
+
```python
|
|
119
|
+
from invisible_playwright.async_api import InvisiblePlaywright
|
|
120
|
+
|
|
121
|
+
async with InvisiblePlaywright(proxy={"server": "socks5://...", "username": "u", "password": "p"}) as browser:
|
|
122
|
+
page = await browser.new_page()
|
|
123
|
+
await page.goto("https://example.com")
|
|
124
|
+
await page.click("#submit")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The `browser` object is a `playwright.sync_api.Browser` / `playwright.async_api.Browser` - every Playwright method works as-is.
|
|
128
|
+
|
|
129
|
+
Log the seed to replay a run:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
sf = InvisiblePlaywright()
|
|
133
|
+
with sf as browser:
|
|
134
|
+
print("seed =", sf.seed)
|
|
135
|
+
# ...
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Reproducible fingerprint
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
with InvisiblePlaywright(seed=42) as browser:
|
|
142
|
+
... # same GPU, same canvas hash, same audio context, every run
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Proxies
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
proxy = {
|
|
149
|
+
"server": "socks5://gate.example.com:1080",
|
|
150
|
+
"username": "user",
|
|
151
|
+
"password": "pass",
|
|
152
|
+
}
|
|
153
|
+
with InvisiblePlaywright(proxy=proxy) as browser:
|
|
154
|
+
...
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Schemes supported: `socks5`, `socks4`, `http`, `https`. DNS is routed through the proxy by default, no local leak.
|
|
158
|
+
|
|
159
|
+
Around 90% of proxies are public, so their IPs are already known and blocked. For the clean 10%, residential IPs that aren't already known, we recommend [sx.org](https://sx.org/?c=invisible_playwright), who filter for and serve only IPs that aren't already on those lists.
|
|
160
|
+
|
|
161
|
+
### Timezone
|
|
162
|
+
|
|
163
|
+
The browser timezone follows `timezone=`:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# default: timezone is auto-derived from the egress IP (proxy egress if a
|
|
167
|
+
# proxy is set, otherwise the host's own public IP)
|
|
168
|
+
with InvisiblePlaywright(proxy=proxy) as browser:
|
|
169
|
+
...
|
|
170
|
+
|
|
171
|
+
# explicit IANA zone always wins, the only way to force a specific zone
|
|
172
|
+
with InvisiblePlaywright(proxy=proxy, timezone="America/New_York") as browser:
|
|
173
|
+
...
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Pinning specific fingerprint fields
|
|
177
|
+
|
|
178
|
+
By default everything comes from `seed`. To force specific values while the rest stays seed-derived:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
with InvisiblePlaywright(
|
|
182
|
+
seed=42,
|
|
183
|
+
pin={
|
|
184
|
+
"gpu.renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 Direct3D11)",
|
|
185
|
+
"gpu.vendor": "Google Inc. (NVIDIA)",
|
|
186
|
+
"screen.width": 2560,
|
|
187
|
+
"screen.height": 1440,
|
|
188
|
+
"hardware.concurrency": 16,
|
|
189
|
+
},
|
|
190
|
+
) as browser:
|
|
191
|
+
...
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Full list of pinnable keys, how pinning interacts with the Bayesian sampler, and common patterns are in **[docs/pinning.md](https://github.com/feder-cr/invisible_playwright/blob/main/docs/pinning.md)**.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## CLI
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
invisible_playwright fetch # download the binary if missing
|
|
202
|
+
invisible_playwright fetch --force # re-download even if cached
|
|
203
|
+
invisible_playwright path # print the absolute path to the cached binary
|
|
204
|
+
invisible_playwright version # wrapper and binary versions
|
|
205
|
+
invisible_playwright clear-cache # remove all cached binaries
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Related projects
|
|
209
|
+
|
|
210
|
+
Related projects that cover similar ground:
|
|
211
|
+
|
|
212
|
+
- **[arkenfox/user.js](https://github.com/arkenfox/user.js)** - Firefox privacy hardening via prefs. invisible_playwright patches C++ where prefs are insufficient.
|
|
213
|
+
- **[LibreWolf](https://librewolf.net)** - Firefox fork with privacy defaults. LibreWolf ships a configured binary; invisible_playwright ships source patches + automation wrapper.
|
|
214
|
+
- **[Camoufox](https://github.com/daijro/camoufox)** - open-source anti-detect Firefox. Patches a wider surface and ships its own fingerprint database; invisible_playwright uses a Bayesian sampler.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT - see [LICENSE](https://github.com/feder-cr/invisible_playwright/blob/main/LICENSE). The patched Firefox binary is distributed under the MPL-2.0 (Firefox upstream license). The C++ patches against mozilla-central that produce that binary are at [feder-cr/firefox_antidetect_patch](https://github.com/feder-cr/firefox_antidetect_patch).
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Disclaimer
|
|
225
|
+
|
|
226
|
+
This project is for educational purposes only. It is provided as-is, with no warranties. I take no responsibility for how it is used. Use it at your own risk and in compliance with the laws of your jurisdiction.
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
<p align="center">
|
|
232
|
+
Built by <a href="https://it.linkedin.com/in/federico-elia-5199951b6">Federico Elia</a>
|
|
233
|
+
<a href="https://it.linkedin.com/in/federico-elia-5199951b6"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/linkedin.svg" alt="LinkedIn"></a>
|
|
234
|
+
</p>
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
<a href="https://github.com/feder-cr/invisible_playwright/actions/workflows/tests.yml"><img src="https://github.com/feder-cr/invisible_playwright/actions/workflows/tests.yml/badge.svg" alt="tests"></a>
|
|
3
|
+
<a href="https://github.com/feder-cr/invisible_playwright/blob/main/LICENSE"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/license.svg" alt="License: MIT"></a>
|
|
4
|
+
<a href="https://www.python.org/downloads/"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/python.svg" alt="Python 3.11+"></a>
|
|
5
|
+
<a href="https://github.com/feder-cr/firefox_antidetect_patch/releases"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/firefox.svg" alt="Firefox 151.0"></a>
|
|
6
|
+
<a href="https://github.com/feder-cr/invisible_playwright/stargazers"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/stars.svg" alt="GitHub stars"></a>
|
|
7
|
+
<a href="https://github.com/feder-cr/firefox_antidetect_patch/releases/tag/usage-counter"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/launches.svg" alt="browser launches"></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<div align="center">
|
|
11
|
+
<picture>
|
|
12
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/feder-cr/invisible_playwright/7a8693c6b4386e9a84dd93bedc479ca8654482e1/docs/banner-dark.png">
|
|
13
|
+
<img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/7a8693c6b4386e9a84dd93bedc479ca8654482e1/docs/banner-light.png" alt="invisible_playwright" width="620">
|
|
14
|
+
</picture>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<h3 align="center">A modified Firefox that passes every bot detection test.</h3>
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
## How it works
|
|
22
|
+
|
|
23
|
+
Anti-bots ask two questions. invisible_playwright answers yes to both.
|
|
24
|
+
|
|
25
|
+
**1. Is this a real browser?** Yes. It is Firefox, patched at the C++ source level.
|
|
26
|
+
|
|
27
|
+
- Fingerprint set inside the engine, not injected into the page: Navigator, screen, GPU/WebGL, Canvas, fonts, audio, WebRTC, timezone, network.
|
|
28
|
+
- No JS shim, no override, no seam to read.
|
|
29
|
+
|
|
30
|
+
**2. Is a real person using it?** Yes. The actions are humanized in the driver.
|
|
31
|
+
|
|
32
|
+
- Every click, hover and drag follows a natural mouse path with human timing, no teleporting cursor.
|
|
33
|
+
- Each input is byte-identical to a real mouse: real input source, pressure, trusted events.
|
|
34
|
+
|
|
35
|
+
Driven by the standard Playwright API. Full breakdown: [feder-cr/firefox_antidetect_patch](https://github.com/feder-cr/firefox_antidetect_patch).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Still seeing captchas or anti-bot? It's the proxy.
|
|
40
|
+
Once the browser is handled it stops being the variable. If you are still getting challenged, the tell is no longer the browser, it is the IP you come from. Around 90% of proxies are public: anyone can rent the same address, so it is already known and sits on the blocked-IP lists sites check. A perfect browser on a known IP still loses.
|
|
41
|
+
|
|
42
|
+
> The fix is the clean 10%, residential IPs that aren't already known. For those we recommend [sx.org](https://sx.org/?c=invisible_playwright), who filter for and serve only IPs that aren't already on those lists.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install invisible-playwright
|
|
50
|
+
python -m invisible_playwright fetch # one-time ~100 MB download, SHA256-verified
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Supported platforms: **Windows x86_64**, **Linux x86_64 / arm64**, **macOS arm64 / x86_64**. On macOS the app is ad-hoc signed (not notarized): if Gatekeeper complains, clear the quarantine flag once with `xattr -dr com.apple.quarantine` on the cached `Firefox.app`.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
### Random fingerprint per session
|
|
59
|
+
**100% Playwright-compatible** - sync and async, all methods, zero API changes. If you already use Playwright, switching is two lines:
|
|
60
|
+
|
|
61
|
+
```diff
|
|
62
|
+
- from playwright.sync_api import sync_playwright
|
|
63
|
+
- with sync_playwright() as p:
|
|
64
|
+
- browser = p.firefox.launch()
|
|
65
|
+
+ from invisible_playwright import InvisiblePlaywright
|
|
66
|
+
+ with InvisiblePlaywright() as browser:
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Every session gets a distinct fingerprint (GPU, audio, fonts, screen, ~400 fields) and Bezier-curve mouse motion.
|
|
70
|
+
|
|
71
|
+
**Sync**
|
|
72
|
+
```python
|
|
73
|
+
from invisible_playwright import InvisiblePlaywright
|
|
74
|
+
|
|
75
|
+
with InvisiblePlaywright(proxy={"server": "socks5://...", "username": "u", "password": "p"}) as browser:
|
|
76
|
+
page = browser.new_page()
|
|
77
|
+
page.goto("https://example.com")
|
|
78
|
+
page.click("#submit") # mouse arcs to the button on a Bezier curve
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Async**
|
|
82
|
+
```python
|
|
83
|
+
from invisible_playwright.async_api import InvisiblePlaywright
|
|
84
|
+
|
|
85
|
+
async with InvisiblePlaywright(proxy={"server": "socks5://...", "username": "u", "password": "p"}) as browser:
|
|
86
|
+
page = await browser.new_page()
|
|
87
|
+
await page.goto("https://example.com")
|
|
88
|
+
await page.click("#submit")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The `browser` object is a `playwright.sync_api.Browser` / `playwright.async_api.Browser` - every Playwright method works as-is.
|
|
92
|
+
|
|
93
|
+
Log the seed to replay a run:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
sf = InvisiblePlaywright()
|
|
97
|
+
with sf as browser:
|
|
98
|
+
print("seed =", sf.seed)
|
|
99
|
+
# ...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Reproducible fingerprint
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
with InvisiblePlaywright(seed=42) as browser:
|
|
106
|
+
... # same GPU, same canvas hash, same audio context, every run
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Proxies
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
proxy = {
|
|
113
|
+
"server": "socks5://gate.example.com:1080",
|
|
114
|
+
"username": "user",
|
|
115
|
+
"password": "pass",
|
|
116
|
+
}
|
|
117
|
+
with InvisiblePlaywright(proxy=proxy) as browser:
|
|
118
|
+
...
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Schemes supported: `socks5`, `socks4`, `http`, `https`. DNS is routed through the proxy by default, no local leak.
|
|
122
|
+
|
|
123
|
+
Around 90% of proxies are public, so their IPs are already known and blocked. For the clean 10%, residential IPs that aren't already known, we recommend [sx.org](https://sx.org/?c=invisible_playwright), who filter for and serve only IPs that aren't already on those lists.
|
|
124
|
+
|
|
125
|
+
### Timezone
|
|
126
|
+
|
|
127
|
+
The browser timezone follows `timezone=`:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
# default: timezone is auto-derived from the egress IP (proxy egress if a
|
|
131
|
+
# proxy is set, otherwise the host's own public IP)
|
|
132
|
+
with InvisiblePlaywright(proxy=proxy) as browser:
|
|
133
|
+
...
|
|
134
|
+
|
|
135
|
+
# explicit IANA zone always wins, the only way to force a specific zone
|
|
136
|
+
with InvisiblePlaywright(proxy=proxy, timezone="America/New_York") as browser:
|
|
137
|
+
...
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Pinning specific fingerprint fields
|
|
141
|
+
|
|
142
|
+
By default everything comes from `seed`. To force specific values while the rest stays seed-derived:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
with InvisiblePlaywright(
|
|
146
|
+
seed=42,
|
|
147
|
+
pin={
|
|
148
|
+
"gpu.renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 Direct3D11)",
|
|
149
|
+
"gpu.vendor": "Google Inc. (NVIDIA)",
|
|
150
|
+
"screen.width": 2560,
|
|
151
|
+
"screen.height": 1440,
|
|
152
|
+
"hardware.concurrency": 16,
|
|
153
|
+
},
|
|
154
|
+
) as browser:
|
|
155
|
+
...
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Full list of pinnable keys, how pinning interacts with the Bayesian sampler, and common patterns are in **[docs/pinning.md](https://github.com/feder-cr/invisible_playwright/blob/main/docs/pinning.md)**.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## CLI
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
invisible_playwright fetch # download the binary if missing
|
|
166
|
+
invisible_playwright fetch --force # re-download even if cached
|
|
167
|
+
invisible_playwright path # print the absolute path to the cached binary
|
|
168
|
+
invisible_playwright version # wrapper and binary versions
|
|
169
|
+
invisible_playwright clear-cache # remove all cached binaries
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Related projects
|
|
173
|
+
|
|
174
|
+
Related projects that cover similar ground:
|
|
175
|
+
|
|
176
|
+
- **[arkenfox/user.js](https://github.com/arkenfox/user.js)** - Firefox privacy hardening via prefs. invisible_playwright patches C++ where prefs are insufficient.
|
|
177
|
+
- **[LibreWolf](https://librewolf.net)** - Firefox fork with privacy defaults. LibreWolf ships a configured binary; invisible_playwright ships source patches + automation wrapper.
|
|
178
|
+
- **[Camoufox](https://github.com/daijro/camoufox)** - open-source anti-detect Firefox. Patches a wider surface and ships its own fingerprint database; invisible_playwright uses a Bayesian sampler.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT - see [LICENSE](https://github.com/feder-cr/invisible_playwright/blob/main/LICENSE). The patched Firefox binary is distributed under the MPL-2.0 (Firefox upstream license). The C++ patches against mozilla-central that produce that binary are at [feder-cr/firefox_antidetect_patch](https://github.com/feder-cr/firefox_antidetect_patch).
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Disclaimer
|
|
189
|
+
|
|
190
|
+
This project is for educational purposes only. It is provided as-is, with no warranties. I take no responsibility for how it is used. Use it at your own risk and in compliance with the laws of your jurisdiction.
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
<p align="center">
|
|
196
|
+
Built by <a href="https://it.linkedin.com/in/federico-elia-5199951b6">Federico Elia</a>
|
|
197
|
+
<a href="https://it.linkedin.com/in/federico-elia-5199951b6"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/linkedin.svg" alt="LinkedIn"></a>
|
|
198
|
+
</p>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "invisible-playwright"
|
|
7
|
+
version = "0.3.5"
|
|
8
|
+
description = "Playwright wrapper for a patched Firefox with deterministic stealth profile."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "feder-cr", email = "85809106+feder-cr@users.noreply.github.com" }]
|
|
13
|
+
keywords = ["playwright", "firefox", "stealth", "anti-detect", "automation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: Microsoft :: Windows",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Testing",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
# Pure config (seed -> fingerprint -> prefs, binary download, proxy, geo) lives
|
|
29
|
+
# in invisible-core since the 2026-07-03 split; it pulls platformdirs/requests/
|
|
30
|
+
# maxminddb/tzdata transitively. The wrapper only adds Playwright.
|
|
31
|
+
#
|
|
32
|
+
# An EXACT specifier, not the bare `invisible-core @ git+...` direct reference
|
|
33
|
+
# this line used to carry. Measured, not assumed: pip is structurally blind to
|
|
34
|
+
# a violated direct reference. A direct reference carries no version specifier,
|
|
35
|
+
# so there is nothing for `pip check` to compare, and it reports "No broken
|
|
36
|
+
# requirements found" (exit 0) on an environment where the wrong core is
|
|
37
|
+
# installed. The same mismatch written as a real version specifier is reported,
|
|
38
|
+
# exit 1. pip also discards the metadata it just built for a moved git tag and
|
|
39
|
+
# leaves direct_url.json recording the OLD commit, so the file meant to be
|
|
40
|
+
# ground truth lies exactly when it matters.
|
|
41
|
+
#
|
|
42
|
+
# Cost, accepted knowingly: the pin literal now moves on every engine release,
|
|
43
|
+
# so a firefox-N bump is a three-repo release instead of a core-only push.
|
|
44
|
+
# invisible-core must be live on the index BEFORE this package is published,
|
|
45
|
+
# or a cold install fails to resolve.
|
|
46
|
+
#
|
|
47
|
+
# This literal is the ONLY place the expected core version is written. The
|
|
48
|
+
# import-time assertion in _pin.py reads it back out of this package's own
|
|
49
|
+
# metadata; nothing in src/ hardcodes a second copy (tests/test_core_pin.py
|
|
50
|
+
# fails if one appears). What stays invisible to install-time machinery is the
|
|
51
|
+
# --no-deps / lockfile residue, and _pin.py is the only layer that sees it.
|
|
52
|
+
"invisible-core==18.0.0",
|
|
53
|
+
# Install-time approximation of the Juggler client range we test against.
|
|
54
|
+
# 1.61 added an isMobile field to Browser.setDefaultViewport that an older
|
|
55
|
+
# Juggler rejects, which kills the session (issue #48). The authoritative
|
|
56
|
+
# range now travels in the seal and is enforced at import by
|
|
57
|
+
# _engine.assert_playwright_range(), so widening it no longer needs a
|
|
58
|
+
# wrapper release; this bound only keeps a fresh install in the ballpark.
|
|
59
|
+
"playwright>=1.55,<=1.61.0",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[project.optional-dependencies]
|
|
63
|
+
dev = ["pytest>=7", "pytest-mock>=3", "responses>=0.24", "build>=1", "pytest-rerunfailures>=14", "playwright>=1.55,<=1.61.0"]
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
markers = [
|
|
67
|
+
"unit: pure-logic tests, no I/O or external deps",
|
|
68
|
+
"integration: multi-module tests, no browser",
|
|
69
|
+
"e2e: requires patched Firefox binary and display",
|
|
70
|
+
"slow: tests that build the wheel - opt-in only",
|
|
71
|
+
"linux_only: tests that require Linux platform",
|
|
72
|
+
]
|
|
73
|
+
addopts = "-m 'not slow and not e2e'"
|
|
74
|
+
# tests/playwright-upstream/ is a vendored Microsoft Playwright test suite
|
|
75
|
+
# used for compatibility verification on demand. It has its own deps
|
|
76
|
+
# (pixelmatch with API not matching our version) and a conftest that fails
|
|
77
|
+
# collection in our env. Run it explicitly with --override-ini for compat
|
|
78
|
+
# audits, not on every push.
|
|
79
|
+
norecursedirs = ["playwright-upstream"]
|
|
80
|
+
|
|
81
|
+
[project.scripts]
|
|
82
|
+
invisible-playwright = "invisible_playwright.cli:main"
|
|
83
|
+
|
|
84
|
+
[project.urls]
|
|
85
|
+
Homepage = "https://github.com/feder-cr/invisible_playwright"
|
|
86
|
+
Repository = "https://github.com/feder-cr/invisible_playwright"
|
|
87
|
+
Issues = "https://github.com/feder-cr/invisible_playwright/issues"
|
|
88
|
+
Changelog = "https://github.com/feder-cr/invisible_playwright/blob/main/CHANGELOG.md"
|
|
89
|
+
Documentation = "https://github.com/feder-cr/invisible_playwright#readme"
|
|
90
|
+
|
|
91
|
+
# No [tool.hatch.metadata] allow-direct-references opt-in: there is no direct
|
|
92
|
+
# reference left in [project].dependencies, and leaving the switch on is what
|
|
93
|
+
# would let one creep back in silently. Without it hatchling refuses to build a
|
|
94
|
+
# direct reference at all, which turns the decision above into a build-time gate.
|
|
95
|
+
|
|
96
|
+
[tool.hatch.build.targets.wheel]
|
|
97
|
+
packages = ["src/invisible_playwright"]
|
|
98
|
+
exclude = ["*.bak", "*-bak"]
|
|
99
|
+
|
|
100
|
+
[tool.hatch.build.targets.sdist]
|
|
101
|
+
include = ["src/invisible_playwright", "tests", "README.md", "LICENSE", "pyproject.toml"]
|
|
102
|
+
# tests/playwright-upstream/ is a vendored copy of microsoft/playwright-python,
|
|
103
|
+
# Apache-2.0, ~322 files. Redistributing it inside an sdist whose only declared
|
|
104
|
+
# licence is MIT misstates the licence of what is being distributed, and a PyPI
|
|
105
|
+
# filename can never be re-uploaded once published.
|
|
106
|
+
exclude = ["tests/playwright-upstream"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""invisible_playwright - Playwright wrapper for a patched Firefox with stealth profile.
|
|
2
|
+
|
|
3
|
+
Quickstart:
|
|
4
|
+
|
|
5
|
+
from invisible_playwright import InvisiblePlaywright
|
|
6
|
+
|
|
7
|
+
with InvisiblePlaywright() as browser: # random seed
|
|
8
|
+
page = browser.new_page()
|
|
9
|
+
page.goto("https://example.com")
|
|
10
|
+
|
|
11
|
+
with InvisiblePlaywright(seed=42) as browser: # deterministic
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
with InvisiblePlaywright(humanize=True) as browser: # human-like cursor motion
|
|
15
|
+
page = browser.new_page()
|
|
16
|
+
page.click("#submit") # expanded into a Bezier trajectory
|
|
17
|
+
"""
|
|
18
|
+
# ── Import-time core assertion, and repair ───────────────────────────────────
|
|
19
|
+
# Runs BEFORE every other import. Two questions, one check: is the installed
|
|
20
|
+
# invisible-core new enough to carry a release seal at all, and is it the exact
|
|
21
|
+
# version this distribution declares in pyproject? The expected version is read
|
|
22
|
+
# back out of our own metadata, so it is written in exactly one place.
|
|
23
|
+
#
|
|
24
|
+
# A mismatch is REPAIRED, not just reported: the declared core is installed and
|
|
25
|
+
# picked up in this process, so the script that just imported us keeps going.
|
|
26
|
+
# That is only sound here, on the first line, while invisible_core has not been
|
|
27
|
+
# imported by anyone yet; _pin.py captures exactly that fact before it imports
|
|
28
|
+
# the core, and the repair refuses itself if the snapshot says otherwise. Every
|
|
29
|
+
# failure of the repair falls back to the message it replaced.
|
|
30
|
+
#
|
|
31
|
+
# The comparison and the repair both live in invisible_core._pin, shared with the
|
|
32
|
+
# profile manager so both products diagnose and fix an environment the same way.
|
|
33
|
+
# Our _pin.py is the floor: it owns the three states a module inside the core
|
|
34
|
+
# cannot report on (core absent, core present but unimportable, core present but
|
|
35
|
+
# too damaged to derive its own version) and delegates everything else.
|
|
36
|
+
from ._pin import enforce_core_pin as _enforce_core_pin
|
|
37
|
+
_enforce_core_pin()
|
|
38
|
+
|
|
39
|
+
from ._engine import assert_playwright_range as _assert_pw_range
|
|
40
|
+
_assert_pw_range()
|
|
41
|
+
|
|
42
|
+
from .config import get_default_args, get_default_stealth_prefs
|
|
43
|
+
from .constants import BINARY_VERSION, FIREFOX_UPSTREAM_VERSION
|
|
44
|
+
from ._geo import GeoTimezoneError, resolve_session_timezone
|
|
45
|
+
from .download import ensure_binary, ensure_geoip_mmdb
|
|
46
|
+
from .launcher import InvisiblePlaywright
|
|
47
|
+
|
|
48
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
__version__ = _pkg_version("invisible-playwright")
|
|
52
|
+
except PackageNotFoundError:
|
|
53
|
+
# Editable / source checkout without an install record: fall back to a
|
|
54
|
+
# marker rather than risk shipping a stale hardcoded string.
|
|
55
|
+
__version__ = "0.0.0+unknown"
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"InvisiblePlaywright",
|
|
59
|
+
"ensure_binary",
|
|
60
|
+
"ensure_geoip_mmdb",
|
|
61
|
+
"get_default_stealth_prefs",
|
|
62
|
+
"get_default_args",
|
|
63
|
+
"resolve_session_timezone",
|
|
64
|
+
"GeoTimezoneError",
|
|
65
|
+
"BINARY_VERSION",
|
|
66
|
+
"FIREFOX_UPSTREAM_VERSION",
|
|
67
|
+
"__version__",
|
|
68
|
+
]
|