Every Byte of your Request Indistinguishable from Chrome.
Bot detection doesn't just check your User-Agent anymore.
It fingerprints your TLS handshake. Your HTTP/2 frames. Your QUIC parameters. The order of your headers. Whether you have a session ticket. Whether your SNI is encrypted.
One mismatch = blocked.
import httpcloak
r = httpcloak.get("https://target.com", preset="chrome-143")That's it. Full browser fingerprint. Every layer.
|
|
|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β WITHOUT SESSION TICKET WITH SESSION TICKET β
β β
β Bot Score: 43 Bot Score: 99 β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββββββββ β
β β New TLS handshake β 0-RTT resumption β
β β Looks like a bot β Looks like returning Chrome β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββ
β ECH (Encrypted Client Hello) β
βββββββββββββββββββββββββββββββββββ€
β WITHOUT: sni=plaintext β
β WITH: sni=encrypted β β
βββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββ
β HTTP/3 Fingerprint Match β
βββββββββββββββββββββββββββββββββββ€
β Protocol: h3 β β
β QUIC Version: 1 β β
β Transport Params: β β
β GREASE Frames: β β
βββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β BOTH LIBRARIES β HTTPCLOAK ONLY β
ββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββ€
β β β
β β TLS fingerprint (JA3/JA4) β β HTTP/3 fingerprinting β
β β HTTP/2 fingerprint β β ECH (encrypted SNI) β
β β Post-quantum TLS β β Session persistence β
β β Bot score: 99 β β 0-RTT resumption β
β β β MASQUE proxy β
β β β Domain fronting β
β β β Certificate pinning β
β β β Go, Python, Node.js, C# β
β β β
ββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββ
pip install httpcloak # Python
npm install httpcloak # Node.js
go get github.com/sardanioss/httpcloak # Go
dotnet add package HttpCloak # C#import httpcloak
# One-liner
r = httpcloak.get("https://example.com", preset="chrome-143")
print(r.text, r.protocol)
# With session (for 0-RTT)
with httpcloak.Session(preset="chrome-143") as session:
session.get("https://cloudflare.com/") # Warm up
session.save("session.json")
# Later
session = httpcloak.Session.load("session.json")
r = session.get("https://target.com/") # Bot score: 99c := client.NewClient("chrome-143")
defer c.Close()
resp, _ := c.Get(context.Background(), "https://example.com", nil)
text, _ := resp.Text()
fmt.Println(text, resp.Protocol)import httpcloak from "httpcloak";
const session = new httpcloak.Session({ preset: "chrome-143" });
const r = await session.get("https://example.com");
console.log(r.text, r.protocol);
session.close();using var session = new Session(Presets.Chrome143);
var r = session.Get("https://example.com");
Console.WriteLine($"{r.Text} {r.Protocol}");Hides which domain you're connecting to from network observers.
session = httpcloak.Session(
preset="chrome-143",
ech_from="cloudflare.com" # Fetches ECH config from DNS
)Cloudflare trace shows sni=encrypted instead of sni=plaintext.
TLS session tickets make you look like a returning visitor.
# Warm up on any Cloudflare site
session.get("https://cloudflare.com/")
session.save("session.json")
# Use on your target
session = httpcloak.Session.load("session.json")
r = session.get("https://target.com/") # Bot score: 99Cross-domain warming works because Cloudflare sites share TLS infrastructure.
Two methods for QUIC through proxies:
| Method | How it works |
|---|---|
| SOCKS5 UDP ASSOCIATE | Proxy relays UDP packets. Most residential proxies support this. |
| MASQUE (CONNECT-UDP) | RFC 9298. Tunnels UDP over HTTP/3. Premium providers only. |
# SOCKS5 with UDP
session = httpcloak.Session(proxy="socks5://user:pass@proxy:1080")
# MASQUE
session = httpcloak.Session(proxy="masque://proxy:443")Known MASQUE providers (auto-detected): Bright Data, Oxylabs, Smartproxy, SOAX.
Connect to a different host than what appears in TLS SNI.
client := httpcloak.NewClient("chrome-143",
httpcloak.WithConnectTo("public-cdn.com", "actual-backend.internal"),
)client.PinCertificate("sha256/AAAA...",
httpcloak.PinOptions{IncludeSubdomains: true})client.OnPreRequest(func(req *http.Request) error {
req.Header.Set("X-Custom", "value")
return nil
})
client.OnPostResponse(func(resp *httpcloak.Response) {
log.Printf("Got %d from %s", resp.StatusCode, resp.FinalURL)
})fmt.Printf("DNS: %dms, TCP: %dms, TLS: %dms, Total: %dms\n",
resp.Timing.DNSLookup,
resp.Timing.TCPConnect,
resp.Timing.TLSHandshake,
resp.Timing.Total)session = httpcloak.Session(preset="chrome-143", http_version="h3") # Force HTTP/3
session = httpcloak.Session(preset="chrome-143", http_version="h2") # Force HTTP/2
session = httpcloak.Session(preset="chrome-143", http_version="h1") # Force HTTP/1.1Auto mode tries HTTP/3 first, falls back gracefully.
# Stream large downloads
with session.get(url, stream=True) as r:
for chunk in r.iter_content(chunk_size=8192):
file.write(chunk)
# Multipart upload
r = session.post(url, files={
"file": ("filename.jpg", file_bytes, "image/jpeg")
})| Preset | Platform | PQ | H3 |
|---|---|---|---|
chrome-143 |
Auto | β | β |
chrome-143-windows |
Windows | β | β |
chrome-143-macos |
macOS | β | β |
chrome-143-linux |
Linux | β | β |
firefox-133 |
Auto | β | β |
chrome-mobile-android |
Android | β | β |
chrome-mobile-ios |
iOS | β | β |
PQ = Post-Quantum (X25519MLKEM768) Β· H3 = HTTP/3
| Tool | Tests |
|---|---|
| tls.peet.ws | JA3, JA4, HTTP/2 Akamai |
| quic.browserleaks.com | HTTP/3 QUIC fingerprint |
| cf.erisa.uk | Cloudflare bot score |
| cloudflare.com/cdn-cgi/trace | ECH status, TLS version |
Custom forks for browser-accurate fingerprinting:
- sardanioss/utls β TLS fingerprinting
- sardanioss/quic-go β HTTP/3 fingerprinting
- sardanioss/net β HTTP/2 frame fingerprinting
MIT License
