Pure Python TLS fingerprint spoofing client with browser challenge fallback.
Project description
██╗ ██╗██╗██████╗ ███████╗██████╗ ████████╗██╗ ███████╗
██║ ██║██║██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██║ ██╔════╝
██║ ██║██║██████╔╝█████╗ ██████╔╝ ██║ ██║ ███████╗
╚██╗ ██╔╝██║██╔═══╝ ██╔══╝ ██╔══██╗ ██║ ██║ ╚════██║
╚████╔╝ ██║██║ ███████╗██║ ██║ ██║ ███████╗███████║
╚═══╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝
Pure Python TLS fingerprint spoofing with browser challenge fallback. No curl_cffi. No Go binary. No excuses.
What is this?
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:
- JA3 / JA4 — The TLS ClientHello fingerprint
- HTTP/2 SETTINGS frames — The window sizes, header table sizes, and frame ordering that real browsers negotiate
- HTTP/2 pseudo-header order — Chrome sends
:method :authority :scheme :path. Firefox does:method :path :authority :scheme. - HTTP header ordering — Because Cloudflare reads your headers like a suspicious bouncer reading a fake ID
The result: your Python script walks up to Cloudflare's velvet rope looking like Chrome in a suit, and gets waved straight through.
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:
- TLS when the site is easy
- Browser when the site needs a challenge solve
- Cache when the site was already solved and the clearance cookies are still valid
Think of it as CycleTLS — but in pure Python, without spawning a Go subprocess, without curl_cffi, and without any of that compiled-binary nonsense.
⚠️ 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.
How It Works
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:
python-requests → JA3: 3b5074b1b5d032e5620f69f9159c1ab7 → BLOCKED
urllib3 → JA3: b32309a26951912be7dba376398abc3b → BLOCKED
Chrome → JA3: browser-like → ALLOWED
ViperTLS → JA3: looks like a real browser → ALLOWED
ViperTLS gets there by:
- Using
ssl.SSLContext.set_ciphers()to set TLS 1.2 cipher order - Using
ctypesto callSSL_CTX_set_ciphersuites()directly on theSSL_CTX*pointer extracted from CPython internals for TLS 1.3 cipher ordering - Using
ctypes→SSL_CTX_set1_groups_list()for elliptic curve ordering - Using the
h2library with custom SETTINGS and Chromium-style behavior injected before the request - Sending HTTP headers in the exact order browsers actually send them
No binary bridge. No subprocess wrapper. Just Python, ctypes, and questionable life choices.
Installation
pip install vipertls
vipertls install-browsers
On Linux:
vipertls install-browsers --with-deps
For a source checkout:
git clone https://github.com/walterwhite-69/ViperTLS
cd ViperTLS
pip install -e .
python install_browsers.py
Quick commands:
vipertls --help
vipertls
vipertls paths
vipertls serve --host 127.0.0.1 --port 5000
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.
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.
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.
Python 3.12 is the recommended runtime. Hosted deployments should prefer Python 3.12. Python 3.13 disables the fragile low-level OpenSSL pointer path to avoid crashes, so browser solving can still work but TLS fingerprint control may be less exact on that runtime.
Quick Start
import asyncio
import vipertls
async def main():
async with vipertls.AsyncClient(impersonate="edge_133", debug_messages=True) as client:
response = await client.get("https://www.crunchyroll.com/")
print(response.status_code)
print(response.solved_by)
print(response.solve_info)
asyncio.run(main())
Ways to Use ViperTLS
ViperTLS can be used in three main ways, depending on what kind of integration you need:
1. As a Python module
Best when you control the Python code directly and want the cleanest API.
import asyncio
import vipertls
async def main():
async with vipertls.AsyncClient(impersonate="edge_133") as client:
response = await client.get("https://example.com")
print(response.status_code)
print(response.solved_by)
print(response.solve_info)
asyncio.run(main())
2. As a local proxy server
Best when the thing making requests cannot import Python code directly, but can send HTTP requests to localhost.
vipertls serve --host 127.0.0.1 --port 8080
Then:
curl http://127.0.0.1:8080 \
-H "X-Viper-URL: https://example.com" \
-H "X-Viper-Impersonate: edge_133"
3. As a standalone browser solver API
Best when you only want the browser-solver side exposed as an API service.
python -m vipertls.solver --port 8081
Then:
curl -X POST http://127.0.0.1:8081/solve \
-H "content-type: application/json" \
-d "{\"url\":\"https://example.com\",\"preset\":\"edge_133\",\"timeout\":30}"
Which one should you use?
- use the Python module if you're already in Python
- use the local proxy server if another tool can only talk HTTP
- use the standalone solver API if you only need challenge solving as a service
Usage
Async Client
import asyncio
import vipertls
async def main():
async with vipertls.AsyncClient(
impersonate="edge_133",
proxy="socks5://user:pass@host:1080",
timeout=30,
verify=True,
follow_redirects=True,
debug_messages=True,
) as client:
r = await client.get("https://example.com/")
print(r.status_code, r.http_version, len(r.content))
print(r.solved_by, r.from_cache)
print(r.cookies_received)
print(r.cookies_used)
asyncio.run(main())
Solver States
When you inspect a response, r.solved_by tells you how ViperTLS got through:
tls— direct request worked immediatelybrowser— direct request hit a challenge and the browser solver resolved itcache— an earlier browser solve already produced valid cookies, so ViperTLS reused them
The extra response metadata is available directly on the Python object:
print(r.solved_by)
print(r.from_cache)
print(r.cookies_received)
print(r.cookies_used)
print(r.solve_info)
Sync Client
import vipertls
client = vipertls.Client(impersonate="firefox_127", timeout=30)
r = client.get("https://www.tempmail.la/")
print(r.status_code)
print(r.text[:500])
Response Object
r.status_code
r.ok
r.solved_by
r.from_cache
r.headers
r.content
r.text
r.json()
r.http_version
r.url
r.cookies_received
r.cookies_used
r.solve_info
r.raise_for_status()
Runtime Helpers
import vipertls
print(vipertls.get_runtime_paths())
vipertls.clear_solver_cache()
vipertls.clear_solver_cache(domain="1337x.to")
vipertls.clear_solver_cache(domain="1337x.to", preset="edge_133")
Live Dashboard (TUI)
A real-time request monitor built with rich.
import asyncio
from vipertls import ViperDashboard
async def main():
async with ViperDashboard(impersonate="chrome_124", timeout=30) as dash:
await asyncio.gather(
dash.get("https://www.crunchyroll.com/"),
dash.get("https://tls.peet.ws/api/all", headers={"accept": "application/json"}),
)
asyncio.run(main())
The dashboard shows live spinners for in-flight requests, color-coded status codes, HTTP version, response size, timing, and preset used.
Server Mode
Run ViperTLS as a local HTTP proxy server. Make requests to localhost with X-Viper-* control headers.
vipertls serve --host 127.0.0.1 --port 8080
vipertls serve --host 0.0.0.0 --port 8080 --workers 4
Then:
curl -s http://localhost:8080 \
-H "X-Viper-URL: https://www.crunchyroll.com/" \
-H "X-Viper-Impersonate: chrome_124"
Control Headers
| Header | Description | Example |
|---|---|---|
X-Viper-URL |
Target URL to request | https://www.crunchyroll.com/api/... |
X-Viper-Method |
HTTP method | POST |
X-Viper-Impersonate |
Browser preset name | chrome_124, firefox_127, safari_17 |
X-Viper-Proxy |
Proxy URL | socks5://user:pass@host:1080 |
X-Viper-Timeout |
Request timeout in seconds | 30 |
X-Viper-JA3 |
Override JA3 fingerprint string | 771,4865-4866-4867,... |
X-Viper-No-Redirect |
Disable redirect following | true |
X-Viper-Skip-Verify |
Skip TLS certificate verification | true |
X-Viper-Force-HTTP1 |
Force HTTP/1.1 | true |
X-Viper-Body |
Request body as string | {"key":"value"} |
X-Viper-Headers |
Extra headers as JSON string | {"authorization":"Bearer ..."} |
The proxy response also includes ViperTLS-specific helper headers such as:
X-ViperTLS-Solved-ByX-Viper-HTTP-VersionX-Viper-Received-CookiesX-ViperTLS-Used-Cookies
Standalone Solver API
python -m vipertls.solver --host 127.0.0.1 --port 8081
Available endpoints:
POST /solveDELETE /cookies/{domain}DELETE /cookiesGET /health
Example:
curl -X POST http://127.0.0.1:8081/solve \
-H "content-type: application/json" \
-d "{\"url\":\"https://nopecha.com/demo/cloudflare\",\"preset\":\"edge_133\",\"timeout\":30}"
Browser Presets
| Preset | Alias | TLS Version | Pseudo-header order |
|---|---|---|---|
chrome_124 |
— | TLS 1.3 | :method :authority :scheme :path |
chrome_131 |
chrome |
TLS 1.3 | :method :authority :scheme :path |
firefox_127 |
firefox |
TLS 1.3 | :method :path :authority :scheme |
safari_17 |
safari |
TLS 1.3 | :method :authority :scheme :path |
Aliases: chrome → chrome_131, firefox → firefox_127, safari → safari_17
Recommended Presets
edge_133— best default when you care about the browser-solver pathchrome_*— good default for TLS-first trafficfirefox_*— useful when you specifically want Firefox-like TLS and HTTP/2 behavior
Proxy Support
AsyncClient(proxy="socks5://username:password@proxy.host:1080")
AsyncClient(proxy="socks5h://username:password@proxy.host:1080")
AsyncClient(proxy="socks4://proxy.host:1080")
AsyncClient(proxy="http://username:password@proxy.host:8080")
AsyncClient(proxy="127.0.0.1:8080")
AsyncClient(proxy="127.0.0.1:8080:user:pass")
If you pass ip:port or ip:port:user:pass, ViperTLS treats it as an HTTP CONNECT proxy automatically.
Error Handling
from vipertls import AsyncClient, ViperHTTPError, ViperConnectionError, ViperTimeoutError
Hosting
Yes, it's hostable. It's a FastAPI server.
Railway / Render
A Procfile is included:
web: python -m vipertls serve --host 0.0.0.0 --port $PORT
Docker
docker build -t vipertls .
docker run -p 8080:8080 vipertls
VPS / Bare Metal
git clone https://github.com/walterwhite-69/ViperTLS && cd ViperTLS
pip install -r requirements.txt
python -m vipertls serve --host 0.0.0.0 --port 8080 --workers 4
Important for Hosted Deployments
- prefer Python
3.12 - Linux browser solving needs Playwright system dependencies
- on Linux, use
vipertls install-browsers --with-depswhen the platform allows it - if the platform blocks system package installation, browser solving may fail even if TLS mode still works
Architecture
AsyncClient.get("https://target.com/")
│
▼
resolve_preset("chrome_124")
│
▼
parse_ja3(preset.ja3) → JA3Spec
│
├── [proxy?] open_tunnel(host, 443, proxy_url)
│
▼
build_ssl_context(preset, ja3)
├─ ctx.set_ciphers(tls12_ciphers)
├─ ctypes → SSL_CTX_set_ciphersuites()
├─ ctypes → SSL_CTX_set1_groups_list()
└─ ctx.set_alpn_protocols(["h2","http/1.1"])
│
▼
ctx.wrap_socket(raw_sock, server_hostname=host)
│
▼
selected_alpn_protocol()
├── "h2" → HTTP2Connection
└── "http/1.1" → http1_request()
Known Limitations & Bugs
- No HTTP/3 / QUIC — not implemented yet
- No connection pooling — each request opens a fresh TLS connection
- ctypes approach is CPython-specific — Python
3.13disables the fragile direct pointer path - No full browser profile emulation — solver is practical, not magic
- Cloudflare behavior changes constantly
Roadmap
- fuller JA4 support
- HTTP/3 / QUIC
- connection pooling and keep-alive
- first-class cookie jar / session management
- WebSocket support
- SSE support
- more browser presets
License
MIT. Do whatever you want with it.
As always, Made By Walter.
Built with Python, ctypes, questionable life choices, and a deep hatred of getting 403'd.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vipertls-0.1.5.tar.gz.
File metadata
- Download URL: vipertls-0.1.5.tar.gz
- Upload date:
- Size: 49.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ade468bb50d625daa8956d4fecfe08c9fb6fcad4ce43e5b280510f8595454d6a
|
|
| MD5 |
e8c729ea471dcc721e48dd300c1e6488
|
|
| BLAKE2b-256 |
29f7f3961fd55b669562ef81e6b44e145c57c447bf34a0ac4512b99b86b03ec7
|
File details
Details for the file vipertls-0.1.5-py3-none-any.whl.
File metadata
- Download URL: vipertls-0.1.5-py3-none-any.whl
- Upload date:
- Size: 49.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
351fc69a9616b63c25eb15e2f3b1a8036f1902eaa5a47794ad69e45a3f739c80
|
|
| MD5 |
13a312161d744c82209e347d841fb5a1
|
|
| BLAKE2b-256 |
013554d0ac427ecaf06a73138f67b7fdc55e8a06b85651303edf0bc6a6595439
|