vipertls 0.1.1__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.
- vipertls-0.1.1/PKG-INFO +750 -0
- vipertls-0.1.1/README.md +723 -0
- vipertls-0.1.1/install_browsers.py +43 -0
- vipertls-0.1.1/pyproject.toml +47 -0
- vipertls-0.1.1/setup.cfg +4 -0
- vipertls-0.1.1/vipertls/__init__.py +45 -0
- vipertls-0.1.1/vipertls/__main__.py +104 -0
- vipertls-0.1.1/vipertls/client.py +518 -0
- vipertls-0.1.1/vipertls/core/__init__.py +3 -0
- vipertls-0.1.1/vipertls/core/http1.py +185 -0
- vipertls-0.1.1/vipertls/core/http2.py +236 -0
- vipertls-0.1.1/vipertls/core/response.py +166 -0
- vipertls-0.1.1/vipertls/core/tls.py +311 -0
- vipertls-0.1.1/vipertls/fingerprints/__init__.py +4 -0
- vipertls-0.1.1/vipertls/fingerprints/ja3.py +124 -0
- vipertls-0.1.1/vipertls/fingerprints/presets.py +719 -0
- vipertls-0.1.1/vipertls/proxy/__init__.py +3 -0
- vipertls-0.1.1/vipertls/proxy/tunnel.py +167 -0
- vipertls-0.1.1/vipertls/runtime.py +94 -0
- vipertls-0.1.1/vipertls/server.py +170 -0
- vipertls-0.1.1/vipertls/solver/__init__.py +3 -0
- vipertls-0.1.1/vipertls/solver/__main__.py +31 -0
- vipertls-0.1.1/vipertls/solver/browser.py +1100 -0
- vipertls-0.1.1/vipertls/solver/server.py +62 -0
- vipertls-0.1.1/vipertls/solver/stealth.py +183 -0
- vipertls-0.1.1/vipertls/tui.py +351 -0
- vipertls-0.1.1/vipertls.egg-info/PKG-INFO +750 -0
- vipertls-0.1.1/vipertls.egg-info/SOURCES.txt +30 -0
- vipertls-0.1.1/vipertls.egg-info/dependency_links.txt +1 -0
- vipertls-0.1.1/vipertls.egg-info/entry_points.txt +3 -0
- vipertls-0.1.1/vipertls.egg-info/requires.txt +10 -0
- vipertls-0.1.1/vipertls.egg-info/top_level.txt +2 -0
vipertls-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vipertls
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Pure Python TLS fingerprint spoofing client with browser challenge fallback.
|
|
5
|
+
Author: vipertls
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/youruser/vipertls
|
|
8
|
+
Keywords: tls,ja3,http2,playwright,cloudflare
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: fastapi>=0.110.0
|
|
18
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
19
|
+
Requires-Dist: h2>=4.1.0
|
|
20
|
+
Requires-Dist: cryptography>=42.0.0
|
|
21
|
+
Requires-Dist: charset-normalizer>=3.3.2
|
|
22
|
+
Requires-Dist: brotli>=1.1.0
|
|
23
|
+
Requires-Dist: zstandard>=0.22.0
|
|
24
|
+
Requires-Dist: playwright>=1.40.0
|
|
25
|
+
Requires-Dist: playwright-stealth>=2.0.0
|
|
26
|
+
Requires-Dist: rich>=13.7.0
|
|
27
|
+
|
|
28
|
+
<div align="center">
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
██╗ ██╗██╗██████╗ ███████╗██████╗ ████████╗██╗ ███████╗
|
|
32
|
+
██║ ██║██║██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██║ ██╔════╝
|
|
33
|
+
██║ ██║██║██████╔╝█████╗ ██████╔╝ ██║ ██║ ███████╗
|
|
34
|
+
╚██╗ ██╔╝██║██╔═══╝ ██╔══╝ ██╔══██╗ ██║ ██║ ╚════██║
|
|
35
|
+
╚████╔╝ ██║██║ ███████╗██║ ██║ ██║ ███████╗███████║
|
|
36
|
+
╚═══╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Pure Python TLS fingerprint spoofing with browser challenge fallback. No curl_cffi. No Go binary. No excuses.**
|
|
40
|
+
|
|
41
|
+
[](https://python.org)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
[](https://http2.github.io)
|
|
44
|
+
[](https://cloudflare.com)
|
|
45
|
+
[](https://github.com)
|
|
46
|
+
[](https://github.com)
|
|
47
|
+
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## What is this?
|
|
53
|
+
|
|
54
|
+
ViperTLS is a **pure Python HTTP client** that makes your requests look like they're coming from a real browser at the TLS level. It spoofs:
|
|
55
|
+
|
|
56
|
+
- **JA3 / JA4** — The TLS ClientHello fingerprint (cipher suites, curves, extensions — all in the exact order a real browser sends them)
|
|
57
|
+
- **HTTP/2 SETTINGS frames** — The window sizes, header table sizes, and frame ordering that real browsers negotiate
|
|
58
|
+
- **HTTP/2 pseudo-header order** — Chrome sends `:method :authority :scheme :path`. Firefox does `:method :path :authority :scheme`. Yes, this actually matters.
|
|
59
|
+
- **HTTP header ordering** — Because Cloudflare reads your headers like a suspicious bouncer reading a fake ID
|
|
60
|
+
|
|
61
|
+
The result: your Python script walks up to Cloudflare's velvet rope looking like Chrome 124 in a suit, and gets waved straight through.
|
|
62
|
+
|
|
63
|
+
When TLS fingerprinting is not enough and a site still throws a browser challenge, ViperTLS can escalate into a real browser solve, capture the useful cookies, and reuse them on later requests. So the practical request flow is:
|
|
64
|
+
|
|
65
|
+
- **TLS** when the site is easy
|
|
66
|
+
- **Browser** when the site needs a challenge solve
|
|
67
|
+
- **Cache** when the site was already solved and the clearance cookies are still valid
|
|
68
|
+
|
|
69
|
+
Think of it as [CycleTLS](https://github.com/Danny-Dasilva/CycleTLS) — but in pure Python, without spawning a Go subprocess, without curl_cffi, and without any of that compiled-binary nonsense.
|
|
70
|
+
|
|
71
|
+
> ⚠️ **Fair warning:** There are probably bugs. TLS fingerprinting is a moving target, Cloudflare updates its detection constantly, and we wrote this in Python instead of something sensible. Use in production at your own risk. You've been warned. We take no responsibility. Good luck. ❤️
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## How It Works
|
|
76
|
+
|
|
77
|
+
Cloudflare and other bot-detection systems don't just look at your User-Agent. They analyze the **actual bytes** of your TLS handshake and HTTP/2 connection setup. Every library has a fingerprint:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
python-requests → JA3: 3b5074b1b5d032e5620f69f9159c1ab7 → BLOCKED
|
|
81
|
+
urllib3 → JA3: b32309a26951912be7dba376398abc3b → BLOCKED
|
|
82
|
+
Chrome 124 → JA3: 03a48f04706e1bd47024208459fbfe91 → ✓ ALLOWED
|
|
83
|
+
ViperTLS → JA3: looks like Chrome 124 → ✓ ALLOWED
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
ViperTLS gets there by:
|
|
87
|
+
|
|
88
|
+
1. Using `ssl.SSLContext.set_ciphers()` to set TLS 1.2 cipher order (OpenSSL preserves it exactly)
|
|
89
|
+
2. Using `ctypes` to call `SSL_CTX_set_ciphersuites()` directly on the `SSL_CTX*` pointer extracted from CPython internals — for TLS 1.3 cipher ordering
|
|
90
|
+
3. Using `ctypes` → `SSL_CTX_set1_groups_list()` for elliptic curve ordering
|
|
91
|
+
4. Using the [`h2`](https://python-hyper.org/projects/h2/) library with custom SETTINGS injected before the connection preface
|
|
92
|
+
5. Sending HTTP headers in the exact order browsers actually send them
|
|
93
|
+
|
|
94
|
+
No binary dependencies. No subprocess bridge. Just Python, ctypes, and a deep understanding of how OpenSSL works internally. Spooky? A little. Does it work? Yes.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Installation
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install vipertls
|
|
102
|
+
vipertls install-browsers
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For a source checkout:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
git clone https://github.com/youruser/vipertls
|
|
109
|
+
cd vipertls
|
|
110
|
+
pip install -e .
|
|
111
|
+
python install_browsers.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Quick commands:**
|
|
115
|
+
```
|
|
116
|
+
vipertls --help
|
|
117
|
+
vipertls
|
|
118
|
+
vipertls paths
|
|
119
|
+
vipertls serve --host 127.0.0.1 --port 5000
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
ViperTLS keeps Playwright browsers, solver cookies, and other writable runtime files in one ViperTLS-managed home directory. In a source checkout that is the repo root. In a pip install it falls back to a per-user writable `vipertls` directory automatically.
|
|
123
|
+
|
|
124
|
+
If the solver cannot find a local Chrome or Edge install, it can bootstrap Playwright Chromium into that same ViperTLS home automatically on first browser solve. You can also do it explicitly with `vipertls --install-browsers`.
|
|
125
|
+
|
|
126
|
+
When you use ViperTLS from your own Python script as a module, it prefers a script-local `.vipertls` folder next to that script, so solver cookies and browser assets stay bundled with the scraper project instead of getting mixed into one global cache.
|
|
127
|
+
|
|
128
|
+
Python 3.10+ required. Works on Windows and Linux. macOS may work, but the browser-solver path is less tested.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Quick Start
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import asyncio
|
|
136
|
+
import vipertls
|
|
137
|
+
|
|
138
|
+
async def main():
|
|
139
|
+
async with vipertls.AsyncClient(impersonate="edge_133", debug_messages=True) as client:
|
|
140
|
+
response = await client.get("https://www.crunchyroll.com/")
|
|
141
|
+
print(response.status_code) # 200, not 403
|
|
142
|
+
print(response.solved_by) # tls / browser / cache
|
|
143
|
+
print(response.solve_info)
|
|
144
|
+
|
|
145
|
+
asyncio.run(main())
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
That's it. If you were using `requests` before, you were getting 403'd and quietly crying about it. Now you're not.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Ways to Use ViperTLS
|
|
153
|
+
|
|
154
|
+
ViperTLS can be used in three main ways, depending on what kind of integration you need:
|
|
155
|
+
|
|
156
|
+
### 1. As a Python module
|
|
157
|
+
|
|
158
|
+
Best when you control the Python code directly and want the cleanest API.
|
|
159
|
+
|
|
160
|
+
Use this when:
|
|
161
|
+
|
|
162
|
+
- you're writing your own Python scraper/client
|
|
163
|
+
- you want direct access to `ViperResponse`
|
|
164
|
+
- you want to inspect `solved_by`, `cookies_received`, `cookies_used`, and `solve_info`
|
|
165
|
+
|
|
166
|
+
Typical shape:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
import asyncio
|
|
170
|
+
import vipertls
|
|
171
|
+
|
|
172
|
+
async def main():
|
|
173
|
+
async with vipertls.AsyncClient(impersonate="edge_133") as client:
|
|
174
|
+
response = await client.get("https://example.com")
|
|
175
|
+
print(response.status_code)
|
|
176
|
+
print(response.solved_by)
|
|
177
|
+
print(response.solve_info)
|
|
178
|
+
|
|
179
|
+
asyncio.run(main())
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 2. As a local proxy server
|
|
183
|
+
|
|
184
|
+
Best when the thing making requests cannot import Python code directly, but can send HTTP requests to `localhost`.
|
|
185
|
+
|
|
186
|
+
Use this when:
|
|
187
|
+
|
|
188
|
+
- you're integrating with OpenBullet-style tools
|
|
189
|
+
- you're routing requests from another app/language
|
|
190
|
+
- you want to control target URL and preset through headers
|
|
191
|
+
|
|
192
|
+
Typical shape:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
vipertls serve --host 127.0.0.1 --port 8080
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Then:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
curl http://127.0.0.1:8080 \
|
|
202
|
+
-H "X-Viper-URL: https://example.com" \
|
|
203
|
+
-H "X-Viper-Impersonate: edge_133"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 3. As a standalone browser solver API
|
|
207
|
+
|
|
208
|
+
Best when you only want the browser-solver side exposed as an API service.
|
|
209
|
+
|
|
210
|
+
Use this when:
|
|
211
|
+
|
|
212
|
+
- you want HTML + cookies from a solved challenge page
|
|
213
|
+
- you want to call the solver separately from the full client/proxy
|
|
214
|
+
- you want a browser-solve worker for another service
|
|
215
|
+
|
|
216
|
+
Typical shape:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
python -m vipertls.solver --port 8081
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Then:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
curl -X POST http://127.0.0.1:8081/solve \
|
|
226
|
+
-H "content-type: application/json" \
|
|
227
|
+
-d "{\"url\":\"https://example.com\",\"preset\":\"edge_133\",\"timeout\":30}"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Which one should you use?
|
|
231
|
+
|
|
232
|
+
- use the **Python module** if you're already in Python
|
|
233
|
+
- use the **local proxy server** if another tool can only talk HTTP
|
|
234
|
+
- use the **standalone solver API** if you only need challenge solving as a service
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Usage
|
|
239
|
+
|
|
240
|
+
### Async Client
|
|
241
|
+
|
|
242
|
+
The primary interface. Fully async, built on `asyncio`.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
import asyncio
|
|
246
|
+
import vipertls
|
|
247
|
+
|
|
248
|
+
async def main():
|
|
249
|
+
async with vipertls.AsyncClient(
|
|
250
|
+
impersonate="edge_133", # best default when browser solving matters
|
|
251
|
+
proxy="socks5://user:pass@host:1080", # optional proxy
|
|
252
|
+
timeout=30, # seconds
|
|
253
|
+
verify=True, # TLS cert verification
|
|
254
|
+
follow_redirects=True,
|
|
255
|
+
debug_messages=True,
|
|
256
|
+
) as client:
|
|
257
|
+
|
|
258
|
+
# GET
|
|
259
|
+
r = await client.get("https://example.com/")
|
|
260
|
+
print(r.status_code, r.http_version, len(r.content))
|
|
261
|
+
print(r.solved_by, r.from_cache)
|
|
262
|
+
print(r.cookies_received)
|
|
263
|
+
print(r.cookies_used)
|
|
264
|
+
|
|
265
|
+
# POST with JSON body
|
|
266
|
+
import json
|
|
267
|
+
r = await client.post(
|
|
268
|
+
"https://api.example.com/login",
|
|
269
|
+
headers={"content-type": "application/json", "accept": "application/json"},
|
|
270
|
+
body=json.dumps({"username": "me", "password": "hunter2"}).encode(),
|
|
271
|
+
)
|
|
272
|
+
data = r.json()
|
|
273
|
+
|
|
274
|
+
# Custom JA3 override (if you want to be specific)
|
|
275
|
+
custom = vipertls.AsyncClient(
|
|
276
|
+
impersonate="chrome_124",
|
|
277
|
+
ja3="771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,18-27-65281-0-23-35-13-16-11-5-10-51-45-43-17513-21,29-23-24,0",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
asyncio.run(main())
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Solver States
|
|
284
|
+
|
|
285
|
+
When you inspect a response, `r.solved_by` tells you how ViperTLS got through:
|
|
286
|
+
|
|
287
|
+
- `tls` — direct request worked immediately
|
|
288
|
+
- `browser` — direct request hit a challenge and the browser solver resolved it
|
|
289
|
+
- `cache` — an earlier browser solve already produced valid cookies, so ViperTLS reused them
|
|
290
|
+
|
|
291
|
+
The extra response metadata is available directly on the Python object:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
print(r.solved_by)
|
|
295
|
+
print(r.from_cache)
|
|
296
|
+
print(r.cookies_received)
|
|
297
|
+
print(r.cookies_used)
|
|
298
|
+
print(r.solve_info)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Sync Client
|
|
302
|
+
|
|
303
|
+
For when asyncio gives you anxiety.
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
import vipertls
|
|
307
|
+
|
|
308
|
+
client = vipertls.Client(impersonate="firefox_127", timeout=30)
|
|
309
|
+
|
|
310
|
+
r = client.get("https://www.tempmail.la/")
|
|
311
|
+
print(r.status_code) # 200
|
|
312
|
+
print(r.text[:500])
|
|
313
|
+
|
|
314
|
+
r2 = client.post(
|
|
315
|
+
"https://api.example.com/data",
|
|
316
|
+
headers={"content-type": "application/json"},
|
|
317
|
+
body=b'{"hello": "world"}',
|
|
318
|
+
)
|
|
319
|
+
print(r2.json())
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Response Object
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
r = await client.get("https://example.com/api/data")
|
|
326
|
+
|
|
327
|
+
r.status_code # int — 200, 403, 429, etc.
|
|
328
|
+
r.ok # bool — True if status < 400
|
|
329
|
+
r.solved_by # str — "tls", "browser", or "cache"
|
|
330
|
+
r.from_cache # bool — True when cached cookies were reused
|
|
331
|
+
r.headers # dict — all lowercase keys
|
|
332
|
+
r.content # bytes — decompressed (gzip / br / deflate handled automatically)
|
|
333
|
+
r.text # str — auto-detected encoding
|
|
334
|
+
r.json() # any — parsed JSON, raises on invalid
|
|
335
|
+
r.http_version # str — "HTTP/2" or "HTTP/1.1"
|
|
336
|
+
r.url # str — final URL after redirects
|
|
337
|
+
r.cookies_received # dict — cookies returned by the site on that response
|
|
338
|
+
r.cookies_used # dict — cookies ViperTLS sent internally
|
|
339
|
+
r.solve_info # dict — grouped ViperTLS metadata
|
|
340
|
+
r.raise_for_status() # raises ViperHTTPError if status >= 400
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Runtime Helpers
|
|
344
|
+
|
|
345
|
+
The top-level module also exposes a few convenience helpers:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
import vipertls
|
|
349
|
+
|
|
350
|
+
print(vipertls.get_runtime_paths())
|
|
351
|
+
vipertls.clear_solver_cache()
|
|
352
|
+
vipertls.clear_solver_cache(domain="1337x.to")
|
|
353
|
+
vipertls.clear_solver_cache(domain="1337x.to", preset="edge_133")
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Live Dashboard (TUI)
|
|
359
|
+
|
|
360
|
+
A beautiful real-time request monitor built with [`rich`](https://github.com/Textualize/rich). Swap `AsyncClient` for `ViperDashboard` and watch the requests roll in.
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
import asyncio
|
|
364
|
+
from vipertls import ViperDashboard
|
|
365
|
+
|
|
366
|
+
async def main():
|
|
367
|
+
async with ViperDashboard(impersonate="chrome_124", timeout=30) as dash:
|
|
368
|
+
# Fire requests — the dashboard updates live as each one completes
|
|
369
|
+
results = await asyncio.gather(
|
|
370
|
+
dash.get("https://www.miruro.to/"),
|
|
371
|
+
dash.get("https://www.crunchyroll.com/"),
|
|
372
|
+
dash.get("https://tls.peet.ws/api/all", headers={"accept": "application/json"}),
|
|
373
|
+
dash.post("https://api.example.com/auth", body=b'{"user":"me"}'),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
asyncio.run(main())
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Run the included demo to see it in action:
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
python demo.py
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
The dashboard shows live spinners for in-flight requests, color-coded status codes, HTTP version, response size, timing, and preset used — all updating in real time.
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
╭─────────────────────────────────────────────────────────────────╮
|
|
389
|
+
│ ⚡ V I P E R TLS v0.1.0 · Live Request Monitor │
|
|
390
|
+
╰─────────────────────────────────────────────────────────────────╯
|
|
391
|
+
◉ 9 requests ✓ 7 ok ✗ 2 failed ⏱ 312ms avg ↓ 187.4 KB
|
|
392
|
+
|
|
393
|
+
Time Method URL Status Proto Size Time Preset
|
|
394
|
+
─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
395
|
+
14:22:09 GET miruro.to/ 200 HTTP/2 20.3 KB 287ms chrome_124
|
|
396
|
+
14:22:09 GET crunchyroll.com/ 200 HTTP/2 15.0 KB 401ms chrome_124
|
|
397
|
+
14:22:08 GET tls.peet.ws/api/all 200 HTTP/2 8.2 KB 198ms chrome_124
|
|
398
|
+
14:22:08 POST api.example.com/auth 401 HTTP/2 185 B 134ms chrome_124
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Server Mode
|
|
404
|
+
|
|
405
|
+
Run ViperTLS as a local HTTP proxy server. Make requests to `localhost` with `X-Viper-*` control headers — useful for integrating with tools that can't use the Python library directly.
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
# Start the server
|
|
409
|
+
vipertls serve --host 127.0.0.1 --port 8080
|
|
410
|
+
|
|
411
|
+
# Or with more workers for concurrent load
|
|
412
|
+
vipertls serve --host 0.0.0.0 --port 8080 --workers 4
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Then make requests to it from anywhere:
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
curl -s http://localhost:8080 \
|
|
419
|
+
-H "X-Viper-URL: https://www.crunchyroll.com/" \
|
|
420
|
+
-H "X-Viper-Impersonate: chrome_124" \
|
|
421
|
+
| head -c 500
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
import requests # ironic
|
|
426
|
+
|
|
427
|
+
r = requests.get("http://localhost:8080", headers={
|
|
428
|
+
"X-Viper-URL": "https://www.miruro.to/",
|
|
429
|
+
"X-Viper-Impersonate": "chrome_124",
|
|
430
|
+
"X-Viper-Proxy": "socks5://user:pass@proxy:1080",
|
|
431
|
+
})
|
|
432
|
+
print(r.status_code)
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Control Headers
|
|
436
|
+
|
|
437
|
+
| Header | Description | Example |
|
|
438
|
+
|--------|-------------|---------|
|
|
439
|
+
| `X-Viper-URL` | **Required.** Target URL to request | `https://www.crunchyroll.com/api/...` |
|
|
440
|
+
| `X-Viper-Method` | HTTP method (default: GET) | `POST` |
|
|
441
|
+
| `X-Viper-Impersonate` | Browser preset name | `chrome_124`, `firefox_127`, `safari_17` |
|
|
442
|
+
| `X-Viper-Proxy` | Proxy URL | `socks5://user:pass@host:1080` |
|
|
443
|
+
| `X-Viper-Timeout` | Request timeout in seconds | `30` |
|
|
444
|
+
| `X-Viper-JA3` | Override JA3 fingerprint string | `771,4865-4866-4867,...` |
|
|
445
|
+
| `X-Viper-No-Redirect` | Disable redirect following | `true` |
|
|
446
|
+
| `X-Viper-Skip-Verify` | Skip TLS certificate verification | `true` |
|
|
447
|
+
| `X-Viper-Force-HTTP1` | Force HTTP/1.1 even if server supports H2 | `true` |
|
|
448
|
+
| `X-Viper-Body` | Request body as string | `{"key":"value"}` |
|
|
449
|
+
| `X-Viper-Headers` | Extra headers as JSON string | `{"authorization":"Bearer ..."}` |
|
|
450
|
+
|
|
451
|
+
All other non-`X-Viper-*` headers you send are forwarded to the target. The response comes back with the target's real status code, headers, and body.
|
|
452
|
+
|
|
453
|
+
The proxy response also includes ViperTLS-specific helper headers such as:
|
|
454
|
+
|
|
455
|
+
- `X-ViperTLS-Solved-By`
|
|
456
|
+
- `X-Viper-HTTP-Version`
|
|
457
|
+
- `X-Viper-Received-Cookies`
|
|
458
|
+
- `X-ViperTLS-Used-Cookies`
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Standalone Solver API
|
|
463
|
+
|
|
464
|
+
If you want only the browser-solver exposed as a small API service, run the solver directly:
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
python -m vipertls.solver --host 127.0.0.1 --port 8081
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Available endpoints:
|
|
471
|
+
|
|
472
|
+
- `POST /solve` — solve one URL and return HTML, cookies, user-agent, method, and elapsed time
|
|
473
|
+
- `DELETE /cookies/{domain}` — clear solver cookies for one domain
|
|
474
|
+
- `DELETE /cookies` — clear all solver cookies
|
|
475
|
+
- `GET /health` — health check
|
|
476
|
+
|
|
477
|
+
Example request:
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
curl -X POST http://127.0.0.1:8081/solve \
|
|
481
|
+
-H "content-type: application/json" \
|
|
482
|
+
-d "{\"url\":\"https://nopecha.com/demo/cloudflare\",\"preset\":\"edge_133\",\"timeout\":30}"
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Example response shape:
|
|
486
|
+
|
|
487
|
+
```json
|
|
488
|
+
{
|
|
489
|
+
"url": "https://example.com",
|
|
490
|
+
"status": 200,
|
|
491
|
+
"html": "<!doctype html>...",
|
|
492
|
+
"cookies": {
|
|
493
|
+
"cf_clearance": "..."
|
|
494
|
+
},
|
|
495
|
+
"user_agent": "Mozilla/5.0 ...",
|
|
496
|
+
"method": "browser",
|
|
497
|
+
"elapsed_ms": 8421.7
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Browser Presets
|
|
504
|
+
|
|
505
|
+
| Preset | Alias | TLS Version | Ciphers | Curves | HTTP/2 Window | Pseudo-header order |
|
|
506
|
+
|--------|-------|-------------|---------|--------|---------------|---------------------|
|
|
507
|
+
| `chrome_120` | — | TLS 1.3 | 16 | X25519, P-256, P-384 | 15,663,105 | `:method :authority :scheme :path` |
|
|
508
|
+
| `chrome_124` | — | TLS 1.3 | 16 | X25519, P-256, P-384 | 15,663,105 | `:method :authority :scheme :path` |
|
|
509
|
+
| `chrome_131` | `chrome` | TLS 1.3 | 16 | X25519, P-256, P-384 | 15,663,105 | `:method :authority :scheme :path` |
|
|
510
|
+
| `firefox_120` | — | TLS 1.3 | 18 | X25519, P-256, P-384, P-521, ffdhe2048, ffdhe3072 | 12,517,377 | `:method :path :authority :scheme` |
|
|
511
|
+
| `firefox_127` | `firefox` | TLS 1.3 | 18 | X25519, P-256, P-384, P-521, ffdhe2048, ffdhe3072 | 12,517,377 | `:method :path :authority :scheme` |
|
|
512
|
+
| `safari_17` | `safari` | TLS 1.3 | 20 | X25519, P-256, P-384, P-521 | 15,663,105 | `:method :authority :scheme :path` |
|
|
513
|
+
|
|
514
|
+
Aliases: `chrome` → `chrome_131`, `firefox` → `firefox_127`, `safari` → `safari_17`
|
|
515
|
+
|
|
516
|
+
```python
|
|
517
|
+
# All valid
|
|
518
|
+
AsyncClient(impersonate="chrome")
|
|
519
|
+
AsyncClient(impersonate="chrome_124")
|
|
520
|
+
AsyncClient(impersonate="firefox_127")
|
|
521
|
+
AsyncClient(impersonate="safari_17")
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Recommended Presets
|
|
525
|
+
|
|
526
|
+
- `edge_133` — best default when you care about the browser-solver path
|
|
527
|
+
- `chrome_*` — good default for TLS-first traffic
|
|
528
|
+
- `firefox_*` — useful when you specifically want Firefox-like TLS and HTTP/2 behavior
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Proxy Support
|
|
533
|
+
|
|
534
|
+
ViperTLS supports all common proxy types. The tunnel is established first, then TLS is wrapped over it — so the fingerprint is still fully intact through the proxy.
|
|
535
|
+
|
|
536
|
+
```python
|
|
537
|
+
# SOCKS5 (with auth)
|
|
538
|
+
AsyncClient(proxy="socks5://username:password@proxy.host:1080")
|
|
539
|
+
|
|
540
|
+
# SOCKS5 with remote DNS (anonymizes DNS leaks)
|
|
541
|
+
AsyncClient(proxy="socks5h://username:password@proxy.host:1080")
|
|
542
|
+
|
|
543
|
+
# SOCKS4
|
|
544
|
+
AsyncClient(proxy="socks4://proxy.host:1080")
|
|
545
|
+
|
|
546
|
+
# HTTP CONNECT proxy
|
|
547
|
+
AsyncClient(proxy="http://username:password@proxy.host:8080")
|
|
548
|
+
|
|
549
|
+
# Short HTTP proxy formats
|
|
550
|
+
AsyncClient(proxy="127.0.0.1:8080")
|
|
551
|
+
AsyncClient(proxy="127.0.0.1:8080:user:pass")
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
If you pass `ip:port` or `ip:port:user:pass`, ViperTLS treats it as an HTTP CONNECT proxy automatically. For SOCKS proxies, keep using the explicit `socks4://`, `socks5://`, or `socks5h://` form.
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## Error Handling
|
|
559
|
+
|
|
560
|
+
```python
|
|
561
|
+
from vipertls import AsyncClient, ViperHTTPError, ViperConnectionError, ViperTimeoutError
|
|
562
|
+
|
|
563
|
+
async with AsyncClient(impersonate="chrome_124") as client:
|
|
564
|
+
try:
|
|
565
|
+
r = await client.get("https://example.com/")
|
|
566
|
+
r.raise_for_status()
|
|
567
|
+
print(r.json())
|
|
568
|
+
except ViperHTTPError as e:
|
|
569
|
+
print(f"Server returned {e.status_code}")
|
|
570
|
+
except ViperTimeoutError:
|
|
571
|
+
print("Timed out — the server is either slow or dead")
|
|
572
|
+
except ViperConnectionError as e:
|
|
573
|
+
print(f"Could not connect: {e}")
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Hosting
|
|
579
|
+
|
|
580
|
+
Yes, it's hostable. It's a FastAPI server — if it runs Python, it runs ViperTLS.
|
|
581
|
+
|
|
582
|
+
### Railway / Render
|
|
583
|
+
|
|
584
|
+
A `Procfile` is included:
|
|
585
|
+
|
|
586
|
+
```
|
|
587
|
+
web: python -m vipertls serve --host 0.0.0.0 --port $PORT
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Push to GitHub, connect to Railway or Render, done. Both platforms pick up `$PORT` automatically.
|
|
591
|
+
|
|
592
|
+
### Docker
|
|
593
|
+
|
|
594
|
+
A `Dockerfile` is included:
|
|
595
|
+
|
|
596
|
+
```bash
|
|
597
|
+
docker build -t vipertls .
|
|
598
|
+
docker run -p 8080:8080 vipertls
|
|
599
|
+
|
|
600
|
+
# With env variable port
|
|
601
|
+
docker run -e PORT=9000 -p 9000:9000 vipertls
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Docker Compose
|
|
605
|
+
|
|
606
|
+
```yaml
|
|
607
|
+
version: "3.9"
|
|
608
|
+
services:
|
|
609
|
+
vipertls:
|
|
610
|
+
build: .
|
|
611
|
+
ports:
|
|
612
|
+
- "8080:8080"
|
|
613
|
+
environment:
|
|
614
|
+
- PORT=8080
|
|
615
|
+
restart: unless-stopped
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### VPS / Bare Metal
|
|
619
|
+
|
|
620
|
+
```bash
|
|
621
|
+
# Install
|
|
622
|
+
git clone https://github.com/youruser/vipertls && cd vipertls
|
|
623
|
+
pip install -r requirements.txt
|
|
624
|
+
|
|
625
|
+
# Run
|
|
626
|
+
python -m vipertls serve --host 0.0.0.0 --port 8080 --workers 4
|
|
627
|
+
|
|
628
|
+
# Or with systemd / screen / tmux / whatever keeps you sane
|
|
629
|
+
screen -S vipertls python -m vipertls serve --host 0.0.0.0 --port 8080
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### Pterodactyl
|
|
633
|
+
|
|
634
|
+
Use the generic Python egg (or any egg that gives you a shell). Startup command:
|
|
635
|
+
|
|
636
|
+
```
|
|
637
|
+
python -m vipertls serve --host 0.0.0.0 --port {{SERVER_PORT}}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Set `SERVER_PORT` as an egg variable. That's it.
|
|
641
|
+
|
|
642
|
+
### ⚠️ Important for Hosted Deployments
|
|
643
|
+
|
|
644
|
+
- TLS fingerprinting requires direct TCP connections. **Do not put ViperTLS behind another HTTP reverse proxy** (nginx, Caddy, etc.) — proxy termination will strip the TLS layer and your fingerprint data becomes irrelevant. Put it behind a TCP passthrough (stream proxy) instead, or expose it directly.
|
|
645
|
+
- Some cloud providers do outbound connection filtering. If you're getting timeouts hitting specific sites from hosted environments, that's likely the issue — not ViperTLS.
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## Architecture
|
|
650
|
+
|
|
651
|
+
```
|
|
652
|
+
AsyncClient.get("https://target.com/")
|
|
653
|
+
│
|
|
654
|
+
▼
|
|
655
|
+
resolve_preset("chrome_124")
|
|
656
|
+
│
|
|
657
|
+
▼
|
|
658
|
+
parse_ja3(preset.ja3) → JA3Spec
|
|
659
|
+
(cipher IDs → OpenSSL names, curve IDs → group names)
|
|
660
|
+
│
|
|
661
|
+
├─── [proxy?] open_tunnel(host, 443, proxy_url)
|
|
662
|
+
│ └─ HTTP CONNECT / SOCKS4 / SOCKS5
|
|
663
|
+
│
|
|
664
|
+
▼
|
|
665
|
+
build_ssl_context(preset, ja3)
|
|
666
|
+
├─ ctx.set_ciphers(tls12_ciphers) ← order preserved by OpenSSL
|
|
667
|
+
├─ ctypes → SSL_CTX_set_ciphersuites() ← TLS 1.3 cipher order
|
|
668
|
+
├─ ctypes → SSL_CTX_set1_groups_list() ← elliptic curve order
|
|
669
|
+
└─ ctx.set_alpn_protocols(["h2","http/1.1"])
|
|
670
|
+
│
|
|
671
|
+
▼
|
|
672
|
+
ctx.wrap_socket(raw_sock, server_hostname=host)
|
|
673
|
+
(TLS handshake — ClientHello looks like real Chrome)
|
|
674
|
+
│
|
|
675
|
+
▼
|
|
676
|
+
check ssl_sock.selected_alpn_protocol()
|
|
677
|
+
│
|
|
678
|
+
├── "h2" → HTTP2Connection
|
|
679
|
+
│ ├─ h2.local_settings.update(settings)
|
|
680
|
+
│ ├─ initiate_connection() (sends SETTINGS frame)
|
|
681
|
+
│ ├─ increment_flow_control_window(15663105)
|
|
682
|
+
│ └─ send_headers(pseudo_headers in Chrome order)
|
|
683
|
+
│
|
|
684
|
+
└── "http/1.1" → http1_request()
|
|
685
|
+
└─ serialize headers in preset.header_order
|
|
686
|
+
│
|
|
687
|
+
▼
|
|
688
|
+
ViperResponse(status, headers, decompressed_body, url, http_version)
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
The ctypes trick for extracting `SSL_CTX*` from Python's `ssl.SSLContext`:
|
|
692
|
+
|
|
693
|
+
```python
|
|
694
|
+
# CPython PySSLContext struct (64-bit):
|
|
695
|
+
# offset 0: ob_refcnt (8 bytes)
|
|
696
|
+
# offset 8: ob_type (8 bytes)
|
|
697
|
+
# offset 16: SSL_CTX* (8 bytes) ← this is what we want
|
|
698
|
+
|
|
699
|
+
raw = (ctypes.c_char * 24).from_address(id(ctx))
|
|
700
|
+
ssl_ctx_ptr = struct.unpack_from("Q", raw, 16)[0]
|
|
701
|
+
|
|
702
|
+
libssl.SSL_CTX_set1_groups_list(ssl_ctx_ptr, b"X25519:P-256:P-384")
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
Is this cursed? Yes. Does it work? Also yes.
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
## Known Limitations & Bugs
|
|
710
|
+
|
|
711
|
+
Because honesty is important (and we're going to find out eventually anyway):
|
|
712
|
+
|
|
713
|
+
- **HTTP/2 SETTINGS values** — The `h2` library applies its own defaults before our custom values in some configurations. The window increment and pseudo-header order (the most important parts for fingerprinting) work correctly. The raw SETTINGS frame values may differ slightly from a real Chrome capture.
|
|
714
|
+
- **No HTTP/3 / QUIC** — Not implemented yet. Sites that exclusively use QUIC will fall back to HTTP/2, which is fine for now.
|
|
715
|
+
- **No connection pooling** — Each request opens a fresh TLS connection. Fast enough for scraping, not ideal for high-frequency trading or something equally unhinged.
|
|
716
|
+
- **No WebSocket / SSE support** — Coming. Maybe.
|
|
717
|
+
- **ctypes approach is CPython 64-bit only** — Works on Linux and macOS (x86_64, arm64). If you're on 32-bit Python for some reason, please update. If you're running PyPy, this will explode. Gracefully, we hope.
|
|
718
|
+
- **No full browser profile emulation** — The solver is practical and effective, but it is not pretending to be a naturally used desktop profile with years of trust history.
|
|
719
|
+
- **Cloudflare behavior changes constantly** — Some sites solve cleanly, some need browser fallback, and some can still become unstable as Cloudflare updates detection logic.
|
|
720
|
+
- **No general-purpose cookie jar API yet** — Solver cache exists and is reusable, but broader cookie/session ergonomics are still evolving.
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
## Roadmap
|
|
725
|
+
|
|
726
|
+
- [ ] JA4 fingerprint support
|
|
727
|
+
- [ ] HTTP/3 / QUIC (aioquic)
|
|
728
|
+
- [ ] Connection pooling and keep-alive
|
|
729
|
+
- [ ] First-class cookie jar / session management
|
|
730
|
+
- [ ] WebSocket support
|
|
731
|
+
- [ ] SSE (Server-Sent Events)
|
|
732
|
+
- [ ] More browser presets (Opera, Chrome Android, Safari iOS)
|
|
733
|
+
- [ ] Automated fingerprint testing against tls.peet.ws / ja3er.com
|
|
734
|
+
- [ ] Richer response metadata and cache/session tooling
|
|
735
|
+
|
|
736
|
+
---
|
|
737
|
+
|
|
738
|
+
## License
|
|
739
|
+
|
|
740
|
+
MIT. Do whatever you want with it. Don't blame us when it breaks.
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
<div align="center">
|
|
745
|
+
|
|
746
|
+
**Built with Python, ctypes, questionable life choices, and a deep hatred of getting 403'd.**
|
|
747
|
+
|
|
748
|
+
*If this saved you from writing a Go wrapper, consider starring the repo.*
|
|
749
|
+
|
|
750
|
+
</div>
|