httpcloak

package module
v1.6.0-beta.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 7, 2026 License: MIT Imports: 22 Imported by: 1

README ΒΆ

httpcloak

Go Reference PyPI npm NuGet

Every Byte of your Request Indistinguishable from Chrome.



The Problem

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 your SNI is encrypted.

One mismatch = blocked.

The Solution

import httpcloak

r = httpcloak.get("https://target.com", preset="chrome-144")

That's it. Full browser transport layer fingerprint.


What Gets Emulated

πŸ” TLS Layer

  • JA3 / JA4 fingerprints
  • GREASE randomization
  • Post-quantum X25519MLKEM768
  • ECH (Encrypted Client Hello)

πŸš€ Transport Layer

  • HTTP/2 SETTINGS frames
  • WINDOW_UPDATE values
  • Stream priorities (HPACK)
  • QUIC transport parameters
  • HTTP/3 GREASE frames

🧠 Header Layer

  • Sec-Fetch-* coherence
  • Client Hints (Sec-Ch-UA)
  • Accept / Accept-Language
  • Header ordering
  • Cookie persistence

Results

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ECH (Encrypted Client Hello)   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  WITHOUT:  sni=plaintext        β”‚
β”‚  WITH:     sni=encrypted   +    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  HTTP/3 Fingerprint Match       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Protocol:        h3       +    β”‚
β”‚  QUIC Version:    1        +    β”‚
β”‚  Transport Params:         +    β”‚
β”‚  GREASE Frames:            +    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

vs curl_cffi

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚        BOTH LIBRARIES          β”‚       HTTPCLOAK ONLY           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                β”‚                                β”‚
β”‚  + TLS fingerprint (JA3/JA4)   β”‚  + HTTP/3 fingerprinting       β”‚
β”‚  + HTTP/2 fingerprint          β”‚  + ECH (encrypted SNI)         β”‚
β”‚  + Post-quantum TLS            β”‚  + MASQUE proxy                β”‚
β”‚  + Bot score: 99               β”‚  + Domain fronting             β”‚
β”‚                                β”‚  + Certificate pinning         β”‚
β”‚                                β”‚  + Go, Python, Node.js, C#     β”‚
β”‚                                β”‚                                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Install

pip install httpcloak        # Python
npm install httpcloak        # Node.js
go get github.com/sardanioss/httpcloak   # Go
dotnet add package HttpCloak # C#

Quick Start

Python

import httpcloak

# Simple request
r = httpcloak.get("https://example.com", preset="chrome-144")
print(r.status_code, r.protocol)

# POST with JSON
r = httpcloak.post("https://httpbin.org/post",
    json={"key": "value"},
    preset="chrome-144"
)

# Custom headers
r = httpcloak.get("https://httpbin.org/headers",
    headers={"X-Custom": "value"},
    preset="chrome-144"
)

Go

import (
    "context"
    "github.com/sardanioss/httpcloak/client"
)

// Simple request
c := client.NewClient("chrome-144")
defer c.Close()

resp, _ := c.Get(ctx, "https://example.com", nil)
body, _ := resp.Text()
fmt.Println(resp.StatusCode, resp.Protocol)

// POST with JSON
jsonBody := []byte(`{"key": "value"}`)
resp, _ = c.Post(ctx, "https://httpbin.org/post",
    bytes.NewReader(jsonBody),
    map[string][]string{"Content-Type": {"application/json"}},
)

// Custom headers
resp, _ = c.Get(ctx, "https://httpbin.org/headers", map[string][]string{
    "X-Custom": {"value"},
})

Node.js

import httpcloak from "httpcloak";

// Simple request
const session = new httpcloak.Session({ preset: "chrome-144" });
const r = await session.get("https://example.com");
console.log(r.statusCode, r.protocol);

// POST with JSON
const r = await session.post("https://httpbin.org/post", {
    json: { key: "value" }
});

// Custom headers
const r = await session.get("https://httpbin.org/headers", {
    headers: { "X-Custom": "value" }
});

session.close();

C#

using HttpCloak;

// Simple request
using var session = new Session(preset: Presets.Chrome144);
var r = session.Get("https://example.com");
Console.WriteLine($"{r.StatusCode} {r.Protocol}");

// POST with JSON
var r = session.PostJson("https://httpbin.org/post",
    new { key = "value" }
);

// Custom headers
var r = session.Get("https://httpbin.org/headers",
    headers: new Dictionary<string, string> { ["X-Custom"] = "value" }
);

Features

πŸ” ECH (Encrypted Client Hello)

Hides which domain you're connecting to from network observers.

session = httpcloak.Session(
    preset="chrome-144",
    ech_from="cloudflare.com"  # Fetches ECH config from DNS
)

Cloudflare trace shows sni=encrypted instead of sni=plaintext.

⚑ Session Resumption (0-RTT)

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: 99

Cross-domain warming works because Cloudflare sites share TLS infrastructure.

🌐 HTTP/3 Through Proxies

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.

Speculative TLS (enabled by default): CONNECT + TLS ClientHello are sent together, saving one proxy round-trip (~25% faster). Disable if you experience issues with certain proxies:

session = httpcloak.Session(proxy="socks5://...", disable_speculative_tls=True)

🎭 Domain Fronting

Connect to a different host than what appears in TLS SNI.

client := httpcloak.NewClient("chrome-144",
    httpcloak.WithConnectTo("public-cdn.com", "actual-backend.internal"),
)

πŸ“Œ Certificate Pinning

client.PinCertificate("sha256/AAAA...",
    httpcloak.PinOptions{IncludeSubdomains: true})

πŸͺ Request Hooks

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)
})

⏱️ Request Timing

fmt.Printf("DNS: %dms, TCP: %dms, TLS: %dms, Total: %dms\n",
    resp.Timing.DNSLookup,
    resp.Timing.TCPConnect,
    resp.Timing.TLSHandshake,
    resp.Timing.Total)

πŸ”„ Protocol Selection

session = httpcloak.Session(preset="chrome-144", http_version="h3")  # Force HTTP/3
session = httpcloak.Session(preset="chrome-144", http_version="h2")  # Force HTTP/2
session = httpcloak.Session(preset="chrome-144", http_version="h1")  # Force HTTP/1.1

Auto mode tries HTTP/3 first, falls back gracefully.

πŸ”€ Runtime Proxy Switching

Switch proxies mid-session without creating new connections. Perfect for proxy rotation.

session = httpcloak.Session(preset="chrome-144")

# Start with direct connection
r = session.get("https://api.ipify.org")
print(f"Direct IP: {r.text}")

# Switch to proxy 1
session.set_proxy("http://proxy1.example.com:8080")
r = session.get("https://api.ipify.org")
print(f"Proxy 1 IP: {r.text}")

# Switch to proxy 2
session.set_proxy("socks5://proxy2.example.com:1080")
r = session.get("https://api.ipify.org")
print(f"Proxy 2 IP: {r.text}")

# Back to direct
session.set_proxy("")

Split proxy configuration - use different proxies for HTTP/2 and HTTP/3:

session = httpcloak.Session(preset="chrome-144")

# TCP proxy for HTTP/1.1 and HTTP/2
session.set_tcp_proxy("http://tcp-proxy.example.com:8080")

# UDP proxy for HTTP/3 (requires SOCKS5 UDP ASSOCIATE or MASQUE)
session.set_udp_proxy("socks5://udp-proxy.example.com:1080")

# Check current configuration
print(session.get_tcp_proxy())  # TCP proxy URL
print(session.get_udp_proxy())  # UDP proxy URL

πŸ“‹ Header Order Customization

Control the order headers are sent for advanced fingerprinting scenarios.

session = httpcloak.Session(preset="chrome-144")

# Get the current header order (from preset)
print(session.get_header_order())

# Set custom header order
session.set_header_order([
    "accept-language", "sec-ch-ua", "accept",
    "sec-fetch-site", "sec-fetch-mode", "user-agent",
    "sec-ch-ua-platform", "sec-ch-ua-mobile"
])

# Make request with custom order
r = session.get("https://example.com")

# Reset to preset's default order
session.set_header_order([])

JavaScript:

session.setHeaderOrder(["accept-language", "sec-ch-ua", "accept", ...]);
console.log(session.getHeaderOrder());
session.setHeaderOrder([]);  // Reset to default

C#:

session.SetHeaderOrder(new[] { "accept-language", "sec-ch-ua", "accept", ... });
Console.WriteLine(string.Join(", ", session.GetHeaderOrder()));
session.SetHeaderOrder(null);  // Reset to default

Go:

c.SetHeaderOrder([]string{"accept-language", "sec-ch-ua", "accept", ...})
fmt.Println(c.GetHeaderOrder())
c.SetHeaderOrder(nil)  // Reset to default

πŸ“€ Streaming & Uploads

# Stream large downloads
stream = session.get_stream("https://example.com/large-file.zip")
print(f"Size: {stream.content_length} bytes")

with open("file.zip", "wb") as f:
    while True:
        chunk = stream.read(8192)
        if not chunk:
            break
        f.write(chunk)
stream.close()

# Iterator pattern
for chunk in session.get_stream(url).iter_content(chunk_size=8192):
    process(chunk)

# Multipart upload
r = session.post(url, files={
    "file": ("filename.jpg", file_bytes, "image/jpeg")
})

πŸ”’ Authentication

# Basic auth
r = httpcloak.get("https://api.example.com/data",
    auth=("username", "password"),
    preset="chrome-144"
)

# Session-level auth
session = httpcloak.Session(
    preset="chrome-144",
    auth=("username", "password")
)

⏰ Timeouts & Retries

# Timeout
session = httpcloak.Session(preset="chrome-144", timeout=30)

# Per-request timeout
r = session.get("https://slow-api.com/data", timeout=60)
// Go: Timeout and retry configuration
client := client.NewClient("chrome-144",
    client.WithTimeout(30 * time.Second),
    client.WithRetry(3),  // Retry 3 times on 429, 500, 502, 503, 504
    client.WithRetryConfig(
        5,                      // Max retries
        500 * time.Millisecond, // Min backoff
        10 * time.Second,       // Max backoff
        []int{429, 503},        // Status codes to retry
    ),
)

🚫 Redirect Control

// Disable automatic redirects
client := client.NewClient("chrome-144",
    client.WithoutRedirects(),
)

resp, _ := client.Get(ctx, "https://example.com/redirect", nil)
fmt.Println(resp.StatusCode)              // 302
fmt.Println(resp.GetHeader("location"))   // Redirect URL

πŸ”ƒ Refresh (Browser Page Refresh)

Simulates a browser page refresh - closes all TCP/QUIC connections but keeps TLS session cache intact. On next request, connections use TLS resumption (like a real browser).

session = httpcloak.Session(preset="chrome-144")

# Make some requests
session.get("https://example.com/page1")
session.get("https://example.com/page2")

# Simulate browser refresh (F5)
session.refresh()

# Next request uses TLS resumption, looks like returning visitor
session.get("https://example.com/page1")

Go:

session := httpcloak.NewSession("chrome-144")
session.Get(ctx, "https://example.com")
session.Refresh()  // Close connections, keep TLS cache
session.Get(ctx, "https://example.com")  // TLS resumption

Node.js:

session.refresh();

C#:

session.Refresh();

🌍 Local Address Binding

Bind outgoing connections to a specific local IP address. Essential for IPv6 rotation scenarios where you have multiple IPs assigned to your machine.

# Bind to specific IPv6 address
session = httpcloak.Session(
    preset="chrome-144",
    local_address="2001:db8::1"
)

# All requests use this source IP
r = session.get("https://api.ipify.org")
print(r.text)  # Shows 2001:db8::1

# IPv4 works too
session = httpcloak.Session(
    preset="chrome-144",
    local_address="192.168.1.100"
)

Go:

session := httpcloak.NewSession("chrome-144",
    httpcloak.WithLocalAddress("2001:db8::1"),
)

Node.js:

const session = new httpcloak.Session({
    preset: "chrome-144",
    localAddress: "2001:db8::1"
});

C#:

var session = new Session(
    preset: Presets.Chrome144,
    localAddress: "2001:db8::1"
);

Note: When a local address is set, target IPs are automatically filtered to match the address family (IPv6 local β†’ only IPv6 targets).

πŸ”‘ TLS Key Logging

Write TLS session keys to a file for traffic decryption in Wireshark. Works with HTTP/1.1, HTTP/2, and HTTP/3.

session = httpcloak.Session(
    preset="chrome-144",
    key_log_file="/tmp/keys.log"
)

# Make requests - keys written to file
session.get("https://example.com")

# In Wireshark: Edit β†’ Preferences β†’ Protocols β†’ TLS β†’ (Pre)-Master-Secret log filename

Go:

session := httpcloak.NewSession("chrome-144",
    httpcloak.WithKeyLogFile("/tmp/keys.log"),
)

Node.js:

const session = new httpcloak.Session({
    preset: "chrome-144",
    keyLogFile: "/tmp/keys.log"
});

C#:

var session = new Session(
    preset: Presets.Chrome144,
    keyLogFile: "/tmp/keys.log"
);

Also supports SSLKEYLOGFILE environment variable (standard NSS Key Log Format).


API Reference

Python

import httpcloak

# Module-level functions
httpcloak.get(url, **kwargs)
httpcloak.post(url, **kwargs)
httpcloak.put(url, **kwargs)
httpcloak.patch(url, **kwargs)
httpcloak.delete(url, **kwargs)
httpcloak.head(url, **kwargs)
httpcloak.options(url, **kwargs)

# Session class
session = httpcloak.Session(
    preset="chrome-144",       # Browser preset (default)
    proxy="socks5://...",      # Proxy URL
    timeout=30,                # Timeout in seconds
    http_version="h3",         # Force protocol: h1, h2, h3, auto
    ech_from="cloudflare.com", # ECH config source
    auth=("user", "pass"),     # Basic auth
)

# Session methods
session.get(url, **kwargs)
session.post(url, data=None, json=None, **kwargs)
session.get_stream(url)        # Streaming download
session.close()

# Proxy switching
session.set_proxy(url)         # Set both TCP and UDP proxy
session.set_tcp_proxy(url)     # Set TCP proxy only (H1/H2)
session.set_udp_proxy(url)     # Set UDP proxy only (H3)
session.get_proxy()            # Get current proxy
session.get_tcp_proxy()        # Get current TCP proxy
session.get_udp_proxy()        # Get current UDP proxy

# Header order customization
session.set_header_order(order)  # Set custom header order (list of lowercase names)
session.get_header_order()       # Get current header order

# Session persistence (0-RTT resumption)
session.save("session.json")   # Save to file
session = Session.load("session.json")  # Load from file
data = session.marshal()       # Export as string
session = Session.unmarshal(data)  # Import from string

# Response object
response.status_code           # HTTP status
response.ok                    # True if status < 400
response.text                  # Body as string
response.content               # Body as bytes
response.json()                # Parse JSON
response.headers               # Response headers
response.protocol              # h1, h2, or h3
response.url                   # Final URL
response.raise_for_status()    # Raise on 4xx/5xx

Go

import "github.com/sardanioss/httpcloak/client"

// Client creation
c := client.NewClient("chrome-144",
    client.WithTimeout(30 * time.Second),
    client.WithProxy("socks5://..."),
    client.WithRetry(3),
    client.WithoutRedirects(),
    client.WithInsecureSkipVerify(),
)
defer c.Close()

// Request methods
resp, err := c.Get(ctx, url, headers)
resp, err := c.Post(ctx, url, body, headers)
resp, err := c.Put(ctx, url, body, headers)
resp, err := c.Delete(ctx, url, headers)

// Advanced request
resp, err := c.Do(ctx, &client.Request{
    Method:        "GET",
    URL:           url,
    Headers:       map[string][]string{},
    Body:          io.Reader,
    Params:        map[string]string{},
    ForceProtocol: client.ProtocolHTTP3,
    FetchMode:     client.FetchModeCORS,
    Referer:       "https://example.com",
})

// Proxy switching
c.SetProxy(url)            // Set both TCP and UDP proxy
c.SetTCPProxy(url)         // Set TCP proxy only (H1/H2)
c.SetUDPProxy(url)         // Set UDP proxy only (H3)
c.GetProxy()               // Get current proxy
c.GetTCPProxy()            // Get current TCP proxy
c.GetUDPProxy()            // Get current UDP proxy

// Session persistence (0-RTT resumption)
c.Save("session.json")     // Save to file
c, _ = client.Load("session.json")  // Load from file
data, _ := c.Marshal()     // Export as string
c, _ = client.Unmarshal(data)  // Import from string

// Response object
resp.StatusCode
resp.Protocol
resp.Headers
resp.Body           // io.ReadCloser
resp.Text()         // (string, error)
resp.Bytes()        // ([]byte, error)
resp.JSON(&v)       // error
resp.GetHeader(key) // string
resp.IsSuccess()    // bool
resp.IsRedirect()   // bool

Node.js

import httpcloak from "httpcloak";

// Session creation
const session = new httpcloak.Session({
    preset: "chrome-144",
    proxy: "socks5://...",
    timeout: 30000,
    httpVersion: "h3",
});

// Async methods
await session.get(url, options)
await session.post(url, { json, data, headers })
await session.put(url, options)
await session.delete(url, options)

// Sync methods
session.getSync(url, options)
session.postSync(url, options)
session.close()

// Proxy switching
session.setProxy(url)          // Set both TCP and UDP proxy
session.setTcpProxy(url)       // Set TCP proxy only (H1/H2)
session.setUdpProxy(url)       // Set UDP proxy only (H3)
session.getProxy()             // Get current proxy
session.getTcpProxy()          // Get current TCP proxy
session.getUdpProxy()          // Get current UDP proxy
session.proxy                  // Property accessor (get/set)

// Session persistence (0-RTT resumption)
session.save("session.json")   // Save to file
session = httpcloak.Session.load("session.json")  // Load from file
const data = session.marshal() // Export as string
session = httpcloak.Session.unmarshal(data)  // Import from string

// Response object
response.statusCode
response.ok
response.text
response.json()
response.headers
response.protocol

C#

using HttpCloak;

// Session creation
var session = new Session(
    preset: Presets.Chrome144,
    proxy: "socks5://...",
    timeout: 30
);

// Request methods
session.Get(url, headers)
session.Post(url, body, headers)
session.PostJson<T>(url, data, headers)
session.Put(url, body, headers)
session.Delete(url)
session.Dispose()

// Proxy switching
session.SetProxy(url)          // Set both TCP and UDP proxy
session.SetTcpProxy(url)       // Set TCP proxy only (H1/H2)
session.SetUdpProxy(url)       // Set UDP proxy only (H3)
session.GetProxy()             // Get current proxy
session.GetTcpProxy()          // Get current TCP proxy
session.GetUdpProxy()          // Get current UDP proxy
session.Proxy                  // Property accessor (get/set)

// Session persistence (0-RTT resumption)
session.Save("session.json")   // Save to file
var session = Session.Load("session.json")  // Load from file
var data = session.Marshal()   // Export as string
var session = Session.Unmarshal(data)  // Import from string

// Response object
response.StatusCode
response.Ok
response.Text
response.Json<T>()
response.Headers
response.Protocol

Browser Presets

Preset Platform PQ H3
chrome-144 Auto βœ… βœ…
chrome-144-windows Windows βœ… βœ…
chrome-144-macos macOS βœ… βœ…
chrome-144-linux Linux βœ… βœ…
chrome-143 Auto βœ… βœ…
chrome-143-windows Windows βœ… βœ…
chrome-143-macos macOS βœ… βœ…
chrome-143-linux Linux βœ… βœ…
firefox-133 Auto ❌ ❌
safari-18 macOS ❌ βœ…
ios-safari-18 iOS ❌ βœ…
android-chrome-144 Android βœ… βœ…
android-chrome-143 Android βœ… βœ…
ios-chrome-144 iOS βœ… βœ…
ios-chrome-143 iOS βœ… βœ…

PQ = Post-Quantum (X25519MLKEM768) Β· H3 = HTTP/3


Testing Tools

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

Dependencies

Custom forks for browser-accurate fingerprinting:


Connect


MIT License

Documentation ΒΆ

Overview ΒΆ

Package httpcloak provides an HTTP client with perfect browser TLS/HTTP fingerprinting.

httpcloak allows you to make HTTP requests that are indistinguishable from real browsers, bypassing TLS fingerprinting, HTTP/2 fingerprinting, and header-based bot detection.

Basic usage:

client := httpcloak.New("chrome-131")
defer client.Close()

resp, err := client.Get(ctx, "https://example.com")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(resp.Body))

With options:

client := httpcloak.New("chrome-131",
    httpcloak.WithTimeout(30*time.Second),
    httpcloak.WithProxy("http://user:pass@proxy:8080"),
)

Index ΒΆ

Constants ΒΆ

View Source
const (
	// HeaderUpstreamProxy is the header name for per-request proxy override (HTTP only).
	// For HTTPS/CONNECT requests, use Proxy-Authorization header instead.
	HeaderUpstreamProxy = "X-Upstream-Proxy"

	// HeaderTLSOnly is the header name for per-request TLS-only mode override.
	// When set to "true", TLS fingerprinting is applied but preset HTTP headers are skipped.
	// When set to "false", normal mode is used (preset headers applied).
	// If not set, uses the proxy's global TLSOnly setting.
	HeaderTLSOnly = "X-HTTPCloak-TlsOnly"

	// HeaderSession is the header name for per-request session selection.
	// When set, the proxy uses the specified session ID to route the request.
	// Sessions must be registered via RegisterSession() before use.
	// Example: X-HTTPCloak-Session: my-session-id
	HeaderSession = "X-HTTPCloak-Session"

	// HeaderScheme upgrades HTTP requests to HTTPS with TLS fingerprinting.
	// When set to "https", LocalProxy converts http:// URLs to https:// and uses
	// Session.DoStream() with full fingerprinting. This allows standard HTTP proxy
	// clients to get HTTPS fingerprinting without using CONNECT tunneling.
	// Example: X-HTTPCloak-Scheme: https
	HeaderScheme = "X-HTTPCloak-Scheme"

	// ProxyAuthScheme is the authentication scheme for upstream proxy selection.
	// Format: "Proxy-Authorization: HTTPCloak http://user:pass@proxy:8080"
	// This works for both HTTP and HTTPS (CONNECT) requests since Proxy-Authorization
	// is sent with CONNECT requests by standard HTTP clients.
	ProxyAuthScheme = "HTTPCloak"
)

Variables ΒΆ

This section is empty.

Functions ΒΆ

func Presets ΒΆ

func Presets() []string

Presets returns available fingerprint presets

Types ΒΆ

type Client ΒΆ

type Client struct {
	// contains filtered or unexported fields
}

Client is an HTTP client with browser fingerprint spoofing

func New ΒΆ

func New(preset string, opts ...Option) *Client

New creates a new HTTP client with the specified browser fingerprint.

Available presets:

  • "chrome-143" (latest, recommended)
  • "chrome-143-windows", "chrome-143-linux", "chrome-143-macos"
  • "chrome-141", "chrome-133", "chrome-131"
  • "ios-chrome-143", "android-chrome-143"
  • "firefox-133"
  • "safari-18", "ios-safari-17"

Example:

client := httpcloak.New("chrome-143")
defer client.Close()

func (*Client) Close ΒΆ

func (c *Client) Close()

Close releases all resources held by the client

func (*Client) Do ΒΆ

func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)

Do executes an HTTP request

func (*Client) Get ΒΆ

func (c *Client) Get(ctx context.Context, url string) (*Response, error)

Get performs a GET request

func (*Client) GetWithHeaders ΒΆ

func (c *Client) GetWithHeaders(ctx context.Context, url string, headers map[string][]string) (*Response, error)

GetWithHeaders performs a GET request with custom headers

func (*Client) Post ΒΆ

func (c *Client) Post(ctx context.Context, url string, body io.Reader, contentType string) (*Response, error)

Post performs a POST request

func (*Client) PostForm ΒΆ

func (c *Client) PostForm(ctx context.Context, url string, body []byte) (*Response, error)

PostForm performs a POST request with form data

func (*Client) PostJSON ΒΆ

func (c *Client) PostJSON(ctx context.Context, url string, body []byte) (*Response, error)

PostJSON performs a POST request with JSON body

type LocalProxy ΒΆ added in v1.5.8

type LocalProxy struct {
	// contains filtered or unexported fields
}

LocalProxy is an HTTP proxy server that forwards requests through httpcloak sessions with TLS fingerprinting.

Architecture: - For HTTP requests: Forwards through httpcloak Session (fingerprinting applied) - For HTTPS (CONNECT): Tunnels TCP (client does TLS, fingerprinting via upstream proxy only)

Usage with C# HttpClient:

proxy := httpcloak.StartLocalProxy(8080, "chrome-143")
defer proxy.Stop()
// Configure HttpClient to use http://localhost:8080 as proxy

func StartLocalProxy ΒΆ added in v1.5.8

func StartLocalProxy(port int, opts ...LocalProxyOption) (*LocalProxy, error)

StartLocalProxy creates and starts a local HTTP proxy on the specified port. The proxy forwards requests through httpcloak sessions with TLS fingerprinting.

Example:

proxy, err := httpcloak.StartLocalProxy(8080, httpcloak.WithProxyPreset("chrome-143"))
if err != nil {
    log.Fatal(err)
}
defer proxy.Stop()
fmt.Printf("Proxy running on port %d\n", proxy.Port())

func (*LocalProxy) GetSession ΒΆ added in v1.5.8

func (p *LocalProxy) GetSession(sessionID string) *Session

GetSession returns a registered session by ID. Returns nil if the session is not found.

func (*LocalProxy) IsRunning ΒΆ added in v1.5.8

func (p *LocalProxy) IsRunning() bool

IsRunning returns whether the proxy is running

func (*LocalProxy) ListSessions ΒΆ added in v1.5.8

func (p *LocalProxy) ListSessions() []string

ListSessions returns all registered session IDs.

func (*LocalProxy) Port ΒΆ added in v1.5.8

func (p *LocalProxy) Port() int

Port returns the port the proxy is listening on

func (*LocalProxy) RegisterSession ΒΆ added in v1.5.8

func (p *LocalProxy) RegisterSession(sessionID string, session *Session) error

RegisterSession registers a session with the given ID for per-request session selection. The session can then be selected via the X-HTTPCloak-Session header. Returns an error if a session with the same ID already exists.

Example:

session := httpcloak.NewSession("chrome-143", httpcloak.WithSessionProxy("..."))
proxy.RegisterSession("session-1", session)
// Client can now use: X-HTTPCloak-Session: session-1

func (*LocalProxy) Stats ΒΆ added in v1.5.8

func (p *LocalProxy) Stats() map[string]interface{}

Stats returns proxy statistics

func (*LocalProxy) Stop ΒΆ added in v1.5.8

func (p *LocalProxy) Stop() error

Stop stops the local proxy server gracefully

func (*LocalProxy) UnregisterSession ΒΆ added in v1.5.8

func (p *LocalProxy) UnregisterSession(sessionID string) *Session

UnregisterSession removes a session from the registry. The session is NOT closed - caller is responsible for closing it. Returns the session if found, nil otherwise.

type LocalProxyConfig ΒΆ added in v1.5.8

type LocalProxyConfig struct {
	// Port to listen on (0 = auto-select)
	Port int

	// Browser fingerprint preset (default: chrome-143)
	Preset string

	// Request timeout
	Timeout time.Duration

	// Maximum concurrent connections
	MaxConnections int

	// Upstream proxy (optional)
	TCPProxy string
	UDPProxy string

	// TLSOnly mode: only apply TLS fingerprinting, pass HTTP headers through unchanged.
	// Useful when the client (e.g., Playwright) already provides authentic browser headers.
	TLSOnly bool

	// SessionCacheBackend is an optional distributed cache for TLS sessions.
	// Enables session sharing across multiple LocalProxy instances.
	SessionCacheBackend transport.SessionCacheBackend

	// SessionCacheErrorCallback is called when backend operations fail.
	SessionCacheErrorCallback transport.ErrorCallback
}

LocalProxyConfig holds configuration for the local proxy

type LocalProxyOption ΒΆ added in v1.5.8

type LocalProxyOption func(*LocalProxyConfig)

LocalProxyOption configures the local proxy

func WithProxyMaxConnections ΒΆ added in v1.5.8

func WithProxyMaxConnections(n int) LocalProxyOption

WithProxyMaxConnections sets the maximum concurrent connections

func WithProxyPreset ΒΆ added in v1.5.8

func WithProxyPreset(preset string) LocalProxyOption

WithProxyPreset sets the browser fingerprint preset

func WithProxySessionCache ΒΆ added in v1.5.8

func WithProxySessionCache(backend transport.SessionCacheBackend, errorCallback transport.ErrorCallback) LocalProxyOption

WithProxySessionCache sets a distributed TLS session cache backend for the proxy. This enables TLS session ticket sharing across multiple proxy instances.

func WithProxyTLSOnly ΒΆ added in v1.5.8

func WithProxyTLSOnly() LocalProxyOption

WithProxyTLSOnly enables TLS-only mode where only TLS fingerprinting is applied. HTTP headers from the client pass through unchanged - useful when using Playwright or other browsers that already provide authentic headers.

func WithProxyTimeout ΒΆ added in v1.5.8

func WithProxyTimeout(d time.Duration) LocalProxyOption

WithProxyTimeout sets the request timeout

func WithProxyUpstream ΒΆ added in v1.5.8

func WithProxyUpstream(tcpProxy, udpProxy string) LocalProxyOption

WithProxyUpstream sets upstream proxy URLs for the httpcloak session

type Option ΒΆ

type Option func(*clientConfig)

Option configures the Client

func WithProxy ΒΆ

func WithProxy(proxyURL string) Option

WithProxy sets an HTTP/HTTPS/SOCKS5 proxy

func WithTimeout ΒΆ

func WithTimeout(d time.Duration) Option

WithTimeout sets the request timeout

type RedirectInfo ΒΆ added in v1.5.1

type RedirectInfo struct {
	StatusCode int
	URL        string
	Headers    map[string][]string // Multi-value headers
}

RedirectInfo contains information about a redirect response

type Request ΒΆ

type Request struct {
	Method  string
	URL     string
	Headers map[string][]string // Multi-value headers (matches http.Header)
	Body    io.Reader           // Streaming body for uploads
	Timeout time.Duration

	// TLSOnly is a per-request override for TLS-only mode.
	// When set to true, preset HTTP headers are NOT applied - only TLS fingerprinting is used.
	// When nil, the session's TLSOnly setting is used.
	// This is useful for LocalProxy where each request can have different TLS-only settings
	// via the X-HTTPCloak-TlsOnly header.
	TLSOnly *bool
}

Request represents an HTTP request

type Response ΒΆ

type Response struct {
	StatusCode int
	Headers    map[string][]string // Multi-value headers (matches http.Header)
	Body       io.ReadCloser       // Streaming body - call Close() when done
	FinalURL   string
	Protocol   string
	History    []*RedirectInfo
	// contains filtered or unexported fields
}

Response represents an HTTP response

func (*Response) Bytes ΒΆ added in v1.5.3

func (r *Response) Bytes() ([]byte, error)

Bytes reads and returns the entire response body. The body can only be read once unless cached.

func (*Response) Close ΒΆ added in v1.5.3

func (r *Response) Close() error

Close closes the response body.

func (*Response) GetHeader ΒΆ added in v1.5.3

func (r *Response) GetHeader(key string) string

GetHeader returns the first value for the given header key.

func (*Response) GetHeaders ΒΆ added in v1.5.3

func (r *Response) GetHeaders(key string) []string

GetHeaders returns all values for the given header key.

func (*Response) JSON ΒΆ added in v1.5.3

func (r *Response) JSON(v interface{}) error

JSON decodes the response body into the given interface.

func (*Response) Text ΒΆ added in v1.5.3

func (r *Response) Text() (string, error)

Text reads and returns the response body as a string.

type Session ΒΆ

type Session struct {
	// contains filtered or unexported fields
}

Session represents a persistent HTTP session with cookie management

func LoadSession ΒΆ added in v1.5.5

func LoadSession(path string) (*Session, error)

LoadSession loads a session from a file

func NewSession ΒΆ

func NewSession(preset string, opts ...SessionOption) *Session

NewSession creates a new persistent session with cookie management

func UnmarshalSession ΒΆ added in v1.5.5

func UnmarshalSession(data []byte) (*Session, error)

UnmarshalSession loads a session from JSON bytes

func (*Session) Close ΒΆ

func (s *Session) Close()

Close closes the session and releases resources

func (*Session) Do ΒΆ

func (s *Session) Do(ctx context.Context, req *Request) (*Response, error)

Do executes a request within the session, maintaining cookies

func (*Session) DoStream ΒΆ added in v1.5.3

func (s *Session) DoStream(ctx context.Context, req *Request) (*StreamResponse, error)

DoStream executes an HTTP request and returns a streaming response The caller is responsible for closing the response when done Note: Streaming does NOT support redirects - use Do() for redirect handling

func (*Session) DoWithBody ΒΆ added in v1.5.3

func (s *Session) DoWithBody(ctx context.Context, req *Request, bodyReader io.Reader) (*Response, error)

DoWithBody executes a request with an io.Reader as the body for streaming uploads

func (*Session) Get ΒΆ

func (s *Session) Get(ctx context.Context, url string) (*Response, error)

Get performs a GET request within the session

func (*Session) GetCookies ΒΆ

func (s *Session) GetCookies() map[string]string

GetCookies returns all cookies stored in the session

func (*Session) GetHeaderOrder ΒΆ added in v1.5.8

func (s *Session) GetHeaderOrder() []string

GetHeaderOrder returns the current header order. Returns preset's default order if no custom order is set.

func (*Session) GetProxy ΒΆ added in v1.5.8

func (s *Session) GetProxy() string

GetProxy returns the current proxy URL (unified proxy or TCP proxy)

func (*Session) GetStream ΒΆ added in v1.5.3

func (s *Session) GetStream(ctx context.Context, url string) (*StreamResponse, error)

GetStream performs a streaming GET request

func (*Session) GetStreamWithHeaders ΒΆ added in v1.5.3

func (s *Session) GetStreamWithHeaders(ctx context.Context, url string, headers map[string][]string) (*StreamResponse, error)

GetStreamWithHeaders performs a streaming GET request with custom headers

func (*Session) GetTCPProxy ΒΆ added in v1.5.8

func (s *Session) GetTCPProxy() string

GetTCPProxy returns the current TCP proxy URL

func (*Session) GetUDPProxy ΒΆ added in v1.5.8

func (s *Session) GetUDPProxy() string

GetUDPProxy returns the current UDP proxy URL

func (*Session) Marshal ΒΆ added in v1.5.5

func (s *Session) Marshal() ([]byte, error)

Marshal exports session state to JSON bytes

func (*Session) Refresh ΒΆ added in v1.5.10

func (s *Session) Refresh()

Refresh closes all connections but keeps TLS session caches and cookies intact. This simulates a browser page refresh - new TCP/QUIC connections but TLS resumption. Useful for resetting connection state without losing session tickets or cookies.

func (*Session) Save ΒΆ added in v1.5.5

func (s *Session) Save(path string) error

Save exports session state (cookies, TLS sessions) to a file

func (*Session) SetCookie ΒΆ

func (s *Session) SetCookie(name, value string)

SetCookie sets a cookie in the session

func (*Session) SetHeaderOrder ΒΆ added in v1.5.8

func (s *Session) SetHeaderOrder(order []string)

SetHeaderOrder sets a custom header order for all requests. Pass nil or empty slice to reset to preset's default order. Order should contain lowercase header names.

func (*Session) SetProxy ΒΆ added in v1.5.8

func (s *Session) SetProxy(proxyURL string)

SetProxy sets or updates the proxy for all protocols (HTTP/1.1, HTTP/2, HTTP/3) This closes existing connections and recreates transports with the new proxy Pass empty string to switch to direct connection

func (*Session) SetSessionIdentifier ΒΆ added in v1.5.8

func (s *Session) SetSessionIdentifier(sessionId string)

SetSessionIdentifier sets a session identifier for TLS cache key isolation. This is used when the session is registered with a LocalProxy to ensure TLS sessions are isolated per proxy/session configuration in distributed caches.

func (*Session) SetTCPProxy ΒΆ added in v1.5.8

func (s *Session) SetTCPProxy(proxyURL string)

SetTCPProxy sets the proxy for TCP protocols (HTTP/1.1, HTTP/2)

func (*Session) SetUDPProxy ΒΆ added in v1.5.8

func (s *Session) SetUDPProxy(proxyURL string)

SetUDPProxy sets the proxy for UDP protocols (HTTP/3 via SOCKS5 or MASQUE)

type SessionOption ΒΆ

type SessionOption func(*sessionConfig)

SessionOption configures a session

func WithConnectTo ΒΆ added in v1.5.2

func WithConnectTo(requestHost, connectHost string) SessionOption

WithConnectTo sets a host mapping for domain fronting. Requests to requestHost will connect to connectHost instead. The TLS SNI and Host header will still use requestHost.

func WithDisableECH ΒΆ added in v1.6.0

func WithDisableECH() SessionOption

WithDisableECH disables ECH (Encrypted Client Hello) lookup for faster first request. ECH is an optional privacy feature that adds ~15-20ms to first connection. Disabling it has no security impact, only privacy implications.

func WithDisableSpeculativeTLS ΒΆ

func WithDisableSpeculativeTLS() SessionOption

WithDisableSpeculativeTLS disables the speculative TLS optimization for proxy connections. By default, httpcloak sends the CONNECT request and TLS ClientHello together to save one round-trip (~25% faster). Disable this if you experience issues with certain proxies.

func WithECHFrom ΒΆ added in v1.5.2

func WithECHFrom(domain string) SessionOption

WithECHFrom sets a domain to fetch ECH config from. Instead of fetching ECH from the target domain's DNS, the config will be fetched from this domain. Useful for Cloudflare domains - use "cloudflare-ech.com" to get ECH config that works for any Cloudflare-proxied domain.

func WithForceHTTP1 ΒΆ added in v1.0.1

func WithForceHTTP1() SessionOption

WithForceHTTP1 forces HTTP/1.1 protocol

func WithForceHTTP2 ΒΆ added in v1.0.1

func WithForceHTTP2() SessionOption

WithForceHTTP2 forces HTTP/2 protocol

func WithForceHTTP3 ΒΆ added in v1.1.1

func WithForceHTTP3() SessionOption

WithForceHTTP3 forces HTTP/3 protocol (QUIC)

func WithInsecureSkipVerify ΒΆ added in v1.0.1

func WithInsecureSkipVerify() SessionOption

WithInsecureSkipVerify disables SSL certificate verification

func WithKeyLogFile ΒΆ added in v1.6.0

func WithKeyLogFile(path string) SessionOption

WithKeyLogFile sets the path to write TLS key log for Wireshark decryption. This overrides the global SSLKEYLOGFILE environment variable for this session.

func WithLocalAddress ΒΆ added in v1.6.0

func WithLocalAddress(addr string) SessionOption

WithLocalAddress binds outgoing connections to a specific local IP address. Useful for IPv6 rotation when you have a large IPv6 prefix and want to rotate source IPs per session. Works with IP_FREEBIND on Linux. Supports both IPv4 and IPv6 addresses (e.g., "192.168.1.100" or "2001:db8::1").

func WithQuicIdleTimeout ΒΆ added in v1.5.8

func WithQuicIdleTimeout(d time.Duration) SessionOption

WithQuicIdleTimeout sets the QUIC connection idle timeout. Default is 30 seconds (matches Chrome). Connections are closed after this duration of inactivity. Set higher values if you need longer-lived HTTP/3 connections with gaps between requests.

func WithRedirects ΒΆ added in v1.0.1

func WithRedirects(follow bool, maxRedirects int) SessionOption

WithRedirects configures redirect behavior

func WithRetry ΒΆ added in v1.0.1

func WithRetry(count int) SessionOption

WithRetry enables retry with default settings

func WithRetryConfig ΒΆ added in v1.0.1

func WithRetryConfig(count int, waitMin, waitMax time.Duration, retryOnStatus []int) SessionOption

WithRetryConfig configures retry behavior

func WithSessionCache ΒΆ added in v1.5.8

func WithSessionCache(backend transport.SessionCacheBackend, errorCallback transport.ErrorCallback) SessionOption

WithSessionCache sets a distributed TLS session cache backend. This enables TLS session ticket sharing across multiple instances (e.g., via Redis). The errorCallback is optional and will be called when backend operations fail.

func WithSessionPreferIPv4 ΒΆ added in v1.1.2

func WithSessionPreferIPv4() SessionOption

WithSessionPreferIPv4 makes the session prefer IPv4 addresses over IPv6. Use this on networks with poor IPv6 connectivity.

func WithSessionProxy ΒΆ

func WithSessionProxy(proxyURL string) SessionOption

WithSessionProxy sets a proxy for the session

func WithSessionTCPProxy ΒΆ added in v1.5.3

func WithSessionTCPProxy(proxyURL string) SessionOption

WithSessionTCPProxy sets a proxy for TCP-based protocols (HTTP/1.1 and HTTP/2). Use this with WithSessionUDPProxy for split proxy configuration.

func WithSessionTimeout ΒΆ

func WithSessionTimeout(d time.Duration) SessionOption

WithSessionTimeout sets the timeout for session requests

func WithSessionUDPProxy ΒΆ added in v1.5.3

func WithSessionUDPProxy(proxyURL string) SessionOption

WithSessionUDPProxy sets a proxy for UDP-based protocols (HTTP/3 via MASQUE). Use this with WithSessionTCPProxy for split proxy configuration.

func WithTLSOnly ΒΆ added in v1.5.8

func WithTLSOnly() SessionOption

WithTLSOnly enables TLS-only mode. In this mode, the preset's TLS fingerprint is used but its default HTTP headers are NOT applied. You must set all headers manually per-request. Useful when you need full control over HTTP headers while keeping the TLS fingerprint.

func WithoutRedirects ΒΆ added in v1.0.1

func WithoutRedirects() SessionOption

WithoutRedirects disables automatic redirect following

func WithoutRetry ΒΆ added in v1.0.5

func WithoutRetry() SessionOption

WithoutRetry explicitly disables retry

type StreamResponse ΒΆ added in v1.5.3

type StreamResponse struct {
	StatusCode    int
	Headers       map[string][]string
	FinalURL      string
	Protocol      string
	ContentLength int64 // -1 if unknown (chunked encoding)
	// contains filtered or unexported fields
}

StreamResponse represents a streaming HTTP response where the body is read incrementally. Use this for large file downloads.

func (*StreamResponse) Close ΒΆ added in v1.5.3

func (r *StreamResponse) Close() error

Close closes the response body - must be called when done

func (*StreamResponse) Read ΒΆ added in v1.5.3

func (r *StreamResponse) Read(p []byte) (n int, err error)

Read reads data from the response body

func (*StreamResponse) ReadAll ΒΆ added in v1.5.3

func (r *StreamResponse) ReadAll() ([]byte, error)

ReadAll reads the entire response body into memory This defeats the purpose of streaming but is useful for small responses

func (*StreamResponse) ReadChunk ΒΆ added in v1.5.3

func (r *StreamResponse) ReadChunk(size int) ([]byte, error)

ReadChunk reads up to size bytes from the response

Directories ΒΆ

Path Synopsis
Package client provides an HTTP client with browser TLS/HTTP fingerprint spoofing.
Package client provides an HTTP client with browser TLS/HTTP fingerprint spoofing.
examples
go-examples/basic command
Example: Basic HTTP requests with httpcloak
Example: Basic HTTP requests with httpcloak
go-examples/browserleaks-ech command
Example: ECH + 0-RTT test with browserleaks using HTTP/3
Example: ECH + 0-RTT test with browserleaks using HTTP/3
go-examples/cloudflare command
Example: Multiple requests to Cloudflare trace endpoint
Example: Multiple requests to Cloudflare trace endpoint
go-examples/high-performance command
Example: High-Performance Downloads
Example: High-Performance Downloads
go-examples/quic-idle-timeout command
Example: QUIC Idle Timeout Configuration
Example: QUIC Idle Timeout Configuration
go-examples/session command
Example: Session management with cookies
Example: Session management with cookies
go-examples/session-resumption command
Example: Session Resumption (0-RTT)
Example: Session Resumption (0-RTT)
go-examples/streaming command
Example: Streaming Downloads with httpcloak
Example: Streaming Downloads with httpcloak
go-examples/tls-only command
Example: TLS-Only Mode with httpcloak
Example: TLS-Only Mode with httpcloak
Package protocol defines the IPC message types for communication between the httpcloak daemon and language SDKs (Python, Node.js, etc.)
Package protocol defines the IPC message types for communication between the httpcloak daemon and language SDKs (Python, Node.js, etc.)
keylog.go provides TLS key logging for traffic analysis with Wireshark.
keylog.go provides TLS key logging for traffic analysis with Wireshark.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL