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.
Files changed (32) hide show
  1. vipertls-0.1.1/PKG-INFO +750 -0
  2. vipertls-0.1.1/README.md +723 -0
  3. vipertls-0.1.1/install_browsers.py +43 -0
  4. vipertls-0.1.1/pyproject.toml +47 -0
  5. vipertls-0.1.1/setup.cfg +4 -0
  6. vipertls-0.1.1/vipertls/__init__.py +45 -0
  7. vipertls-0.1.1/vipertls/__main__.py +104 -0
  8. vipertls-0.1.1/vipertls/client.py +518 -0
  9. vipertls-0.1.1/vipertls/core/__init__.py +3 -0
  10. vipertls-0.1.1/vipertls/core/http1.py +185 -0
  11. vipertls-0.1.1/vipertls/core/http2.py +236 -0
  12. vipertls-0.1.1/vipertls/core/response.py +166 -0
  13. vipertls-0.1.1/vipertls/core/tls.py +311 -0
  14. vipertls-0.1.1/vipertls/fingerprints/__init__.py +4 -0
  15. vipertls-0.1.1/vipertls/fingerprints/ja3.py +124 -0
  16. vipertls-0.1.1/vipertls/fingerprints/presets.py +719 -0
  17. vipertls-0.1.1/vipertls/proxy/__init__.py +3 -0
  18. vipertls-0.1.1/vipertls/proxy/tunnel.py +167 -0
  19. vipertls-0.1.1/vipertls/runtime.py +94 -0
  20. vipertls-0.1.1/vipertls/server.py +170 -0
  21. vipertls-0.1.1/vipertls/solver/__init__.py +3 -0
  22. vipertls-0.1.1/vipertls/solver/__main__.py +31 -0
  23. vipertls-0.1.1/vipertls/solver/browser.py +1100 -0
  24. vipertls-0.1.1/vipertls/solver/server.py +62 -0
  25. vipertls-0.1.1/vipertls/solver/stealth.py +183 -0
  26. vipertls-0.1.1/vipertls/tui.py +351 -0
  27. vipertls-0.1.1/vipertls.egg-info/PKG-INFO +750 -0
  28. vipertls-0.1.1/vipertls.egg-info/SOURCES.txt +30 -0
  29. vipertls-0.1.1/vipertls.egg-info/dependency_links.txt +1 -0
  30. vipertls-0.1.1/vipertls.egg-info/entry_points.txt +3 -0
  31. vipertls-0.1.1/vipertls.egg-info/requires.txt +10 -0
  32. vipertls-0.1.1/vipertls.egg-info/top_level.txt +2 -0
@@ -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
+ [![Python](https://img.shields.io/badge/Python-3.10%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://python.org)
42
+ [![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)](LICENSE)
43
+ [![HTTP/2](https://img.shields.io/badge/HTTP%2F2-✓-brightgreen?style=flat-square)](https://http2.github.io)
44
+ [![Cloudflare](https://img.shields.io/badge/Cloudflare-bypassed-orange?style=flat-square&logo=cloudflare&logoColor=white)](https://cloudflare.com)
45
+ [![Bugs](https://img.shields.io/badge/bugs-probably%20some-red?style=flat-square)](https://github.com)
46
+ [![Vibes](https://img.shields.io/badge/vibes-immaculate-purple?style=flat-square)](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>