Skip to main content

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.

Python License HTTP/2 Cloudflare Bugs Vibes


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:

  1. Using ssl.SSLContext.set_ciphers() to set TLS 1.2 cipher order
  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
  3. Using ctypesSSL_CTX_set1_groups_list() for elliptic curve ordering
  4. Using the h2 library with custom SETTINGS and Chromium-style behavior injected before the request
  5. 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 immediately
  • browser — direct request hit a challenge and the browser solver resolved it
  • cache — 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-By
  • X-Viper-HTTP-Version
  • X-Viper-Received-Cookies
  • X-ViperTLS-Used-Cookies

Standalone Solver API

python -m vipertls.solver --host 127.0.0.1 --port 8081

Available endpoints:

  • POST /solve
  • DELETE /cookies/{domain}
  • DELETE /cookies
  • GET /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: chromechrome_131, firefoxfirefox_127, safarisafari_17

Recommended Presets

  • edge_133 — best default when you care about the browser-solver path
  • chrome_* — good default for TLS-first traffic
  • firefox_* — 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-deps when 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.13 disables 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

vipertls-0.1.5.tar.gz (49.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vipertls-0.1.5-py3-none-any.whl (49.9 kB view details)

Uploaded Python 3

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

Hashes for vipertls-0.1.5.tar.gz
Algorithm Hash digest
SHA256 ade468bb50d625daa8956d4fecfe08c9fb6fcad4ce43e5b280510f8595454d6a
MD5 e8c729ea471dcc721e48dd300c1e6488
BLAKE2b-256 29f7f3961fd55b669562ef81e6b44e145c57c447bf34a0ac4512b99b86b03ec7

See more details on using hashes here.

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

Hashes for vipertls-0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 351fc69a9616b63c25eb15e2f3b1a8036f1902eaa5a47794ad69e45a3f739c80
MD5 13a312161d744c82209e347d841fb5a1
BLAKE2b-256 013554d0ac427ecaf06a73138f67b7fdc55e8a06b85651303edf0bc6a6595439

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page