tlsforge

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

README

TLS Forge

tls-forge is an HTTP client and command-line tool for making requests with a real browser's network fingerprint. A measured profile controls the TLS ClientHello, JA3/JA4-relevant fields, HTTP/1.1 header casing and order, HTTP/2 settings, pseudo-header order, regular header order and browser headers as one coherent identity. Profiles created before HTTP/1.1 capture was introduced remain compatible and use the documented canonical-casing fallback.

Use it when an ordinary Go, Node.js or Python HTTP client is rejected because its network stack does not match the browser named by its User-Agent. tls-forge can be used as:

  • a standalone CLI for one request or a batch;
  • an intercepting proxy for an existing program;
  • a Go library;
  • a Node.js or Python SDK backed by the same Go transport;
  • a local measurement tool for capturing and comparing browser fingerprints.

It does not execute page JavaScript and does not solve managed challenges or CAPTCHAs. It addresses the TLS and HTTP fingerprint checks that happen before, or alongside, normal HTTP handling.

What is a TLS fingerprint?

Before an HTTPS client can send GET, headers or a User-Agent, it sends a TLS ClientHello. That message exposes choices made by its networking stack, including:

  • supported TLS versions and cipher suites;
  • extensions and their order;
  • supported groups, key shares and signature algorithms;
  • GREASE values and ALPN protocols;
  • session-resumption and application-settings behaviour.

Servers can retain the complete structure or reduce it to identifiers such as JA3 and JA4. The negotiated HTTP protocol adds more signals: exact HTTP/1.1 header casing and order, or HTTP/2 SETTINGS, window updates, pseudo-header order, normal header order and values.

Changing only User-Agent is therefore insufficient. Node.js and Python normally use OpenSSL, Go uses crypto/tls, while Chrome uses BoringSSL. Their handshakes differ before the server sees any HTTP header.

Cloudflare uses these signals in its bot-detection products. Cloudflare Bot Management exposes JA3 and JA4 fingerprints as request fields that can be used in WAF custom rules, Transform Rules and Workers; see Cloudflare's official JA3/JA4 fingerprint documentation.

The profiles in this project are measurements, not hand-written guesses. tls-forge compare measures a real browser and this client against the same local echo server and reports every difference it finds.

Install

A Chromium-based browser is required only for capture and compare. Fetching, batch processing, proxying and the language SDKs do not require a browser.

Release binary

Prebuilt binaries are published for macOS and Linux on arm64 and x86-64, and Windows on x86-64. Download the archive for your platform from GitHub Releases, extract it and place tls-forge (or tls-forge.exe) on PATH.

On Unix-like systems, a user-local installation can look like this:

mkdir -p ~/.local/bin
install -m 0755 tls-forge ~/.local/bin/tls-forge
tls-forge version
Homebrew

On macOS or Linux:

brew tap Sec-CH-Lemon/tap
brew trust --formula Sec-CH-Lemon/tap/tls-forge
brew install Sec-CH-Lemon/tap/tls-forge

Homebrew requires explicit trust for third-party taps. Review the tap before granting it.

Go install

With Go 1.26.7 or newer:

go install github.com/Sec-CH-Lemon/tls-forge/cmd/tls-forge@latest

The binary is written to $(go env GOPATH)/bin unless GOBIN is set.

Docker

Release images are published for linux/amd64 and linux/arm64:

docker pull ghcr.io/sec-ch-lemon/tls-forge
docker run --rm ghcr.io/sec-ch-lemon/tls-forge \
  fetch https://tls.browserleaks.com/json

Mount a directory when a command reads or writes files:

docker run --rm -v "$PWD:/work" ghcr.io/sec-ch-lemon/tls-forge \
  batch --input /work/urls.csv --output /work/results.jsonl

capture and compare are intentionally not supported by the small release image because it does not contain a browser.

Build from source

Requirements:

  • Go 1.26.7 or newer;
  • Git and Make for the standard build command;
  • no C compiler or cgo.
git clone https://github.com/Sec-CH-Lemon/tls-forge.git
cd tls-forge
make build
./bin/tls-forge version

make build writes bin/tls-forge and stamps its version from git describe. Without Make:

go build -o tls-forge ./cmd/tls-forge
./tls-forge version

A plain go build reports version dev, because release versions are supplied through linker flags. A release-style static build is:

CGO_ENABLED=0 go build -trimpath \
  -ldflags "-s -w -X main.version=v0.1.0" \
  -o tls-forge ./cmd/tls-forge

Go can cross-compile the project without an external toolchain:

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
  go build -o tls-forge-linux-arm64 ./cmd/tls-forge

CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
  go build -o tls-forge.exe ./cmd/tls-forge

To build the container image:

docker build -t tls-forge .
docker run --rm tls-forge version

Run all project checks before distributing a build:

make check

Verify the fingerprint

If a Chromium-based browser is installed, start with:

tls-forge compare

The command measures the browser and the selected profile locally, then compares TLS, JA4, HTTP/2 settings, header order and header values. A matching result is stronger than checking only a JA3 or JA4 hash.

Commands

tls-forge <command> [flags]
Command Purpose
fetch Make one request with a browser fingerprint
batch Fetch many URLs concurrently, optionally with one proxy per URL
capture Measure an installed browser and optionally save a reusable profile
compare Compare a profile with an installed browser
profiles List available profiles
proxy Re-send requests from another program using the selected fingerprint
serve Run the local TLS/HTTP fingerprint echo server
daemon Expose the client as a JSON Lines transport
version Print the build version

Run tls-forge <command> --help for generated command help. Durations use Go syntax such as 500ms, 30s or 2m.

When no profile is selected, the network commands (fetch, batch, proxy and daemon) use a profile installed as local, if one exists, and otherwise the latest bundled chrome profile. compare defaults to the bundled chrome family so it can detect drift instead of comparing a fresh local measurement with itself. Use tls-forge profiles to see the exact names available on the machine.

fetch

Make one request and write its decompressed body to standard output.

tls-forge fetch https://example.com/
tls-forge fetch -i -H "Accept-Language: en-US" https://example.com/
tls-forge fetch -X POST -H "Content-Type: application/json" \
  -d '{"hello":"world"}' https://example.com/api

Usage: tls-forge fetch [flags] <url>

Flag or argument Meaning
<url> Required HTTP or HTTPS URL
-b, --cookie name=value Seed one cookie; repeatable
--cookies <file> Load a warmed session from JSON, a browser export, or Netscape cookies.txt
--cookie-set <id> Select a set from a multi-session JSON file; otherwise one is chosen at random
--save-cookies <file> Atomically save the final session; .txt selects Netscape format
-p, --profile <name-or-path> Profile to impersonate
-x, --proxy <url> HTTP, HTTPS or SOCKS proxy, optionally with credentials
-t, --timeout <duration> Request timeout; default 30s
-k, --insecure Skip upstream certificate verification
-X, --method <method> HTTP method; default GET
-d, --data <text> Request body
-i, --include Print status and response headers before the body
-o, --output <file> Write the body to a file instead of stdout
-H, --header "Name: value" Add or override an ordered request header; repeatable

Cookie files are treated as credentials and saved with mode 0600. JSON files can retain multiple named sessions; Netscape .txt files contain one jar and are compatible with tools such as curl and wget.

batch

Fetch a list concurrently and emit one JSON object per URL. Results go to stdout or --output; progress and summaries go to stderr, so the JSON Lines stream remains machine-readable.

tls-forge batch https://example.com/ https://example.org/
tls-forge batch --input example/urls.csv --concurrency 8
tls-forge batch --input urls.json --output results.jsonl --report reports/

Usage: tls-forge batch [flags] [urls...]

Flag or argument Meaning
[urls...] URLs passed directly when neither --urls nor --input is used
-b, --cookie name=value Seed one cookie; repeatable
--cookies <file> Load warmed cookies
--cookie-set <id> Select one set from the cookie file
--save-cookies <file> Save one resulting session per distinct proxy
-p, --profile <name-or-path> Profile to impersonate
-x, --proxy <url> Default proxy for entries that do not specify one
-t, --timeout <duration> Per-request timeout; default 30s
-k, --insecure Skip upstream certificate verification
-i, --input <file> Read URLs from a file; stdin is used when no source is given
-u, --urls <a,b,c> Comma-separated URLs; highest input precedence
-F, --format <format> auto, lines, json or csv; default auto
-c, --concurrency <n> Concurrent requests; default 4
-o, --output <file> Write JSON Lines results here instead of stdout
-d, --body-dir <dir> Store response bodies separately and reference their paths
--repeat <n> Number of retries after a URL does not load; default 3
-v, --verbose Print one status line when each request finishes
--progress <mode> Live stderr status: auto, always or never; default auto
-R, --report <path> Write a self-contained HTML report; a directory gets a timestamped filename
--report-ip[=false] Ask https://ipinfo.io/json once per proxy for report metadata; default true

Input precedence is --urls, then --input, then positional URLs, then stdin. When an inline response body is not valid UTF-8, body is base64 and the row contains "body_encoding":"base64"; bytes always remains the original byte count. --body-dir writes the original bytes directly instead. Repeated URLs in one input get separate, stable body filenames. With --format auto, .json selects JSON, .csv selects CSV and every other name uses one URL per line.

Supported list shapes:

# lines: URL followed by an optional proxy
https://example.com/ http://user:pass@proxy.example:8080
[
  "https://example.com/",
  {"url": "https://example.org/", "proxy": "socks5://proxy.example:1080"}
]
url,proxy
https://example.com/,http://user:pass@proxy.example:8080
https://example.org/,

The command creates one client and cookie jar per distinct proxy. A batch exits non-zero when one or more URLs fail.

Runnable CLI and SDK examples, with complete sample inputs and outputs, are in example/.

capture

Open an installed Chromium-based browser, record its TLS and HTTP behaviour and optionally save a reusable profile. HTTP/1.1 and HTTP/2 are measured as two independent cold top-level navigations, each using a fresh temporary browser profile, so the transition between protocols cannot alter Sec-Fetch-*.

tls-forge capture
mkdir -p profiles && tls-forge capture --save profiles/
tls-forge capture --browser edge --headless --save edge.json
tls-forge capture --install

Usage: tls-forge capture [flags]

Flag Meaning
-b, --browser <name-or-path> chrome, chromium, edge, brave, or an executable path; auto-detected when omitted
--headless Run without a window; TLS/HTTP structure remains useful for drift checks, but Chrome may expose HeadlessChrome and therefore differ in header values from a headed profile
-s, --save <path> Save a profile; an existing directory gets <name>/<platform>.json
--install Save into the user profile directory so --profile local can find it
-n, --name <name> Override the profile name; otherwise derive it from the browser version
-j, --json Print the raw capture instead of a human summary
-t, --timeout <duration> Maximum browser measurement time; default 2m
--browser-arg <argument> Append a browser process argument; repeatable, mainly for CI environments

Installed profiles live below the operating system's user configuration directory. Set TLSFORGE_PROFILES to choose another directory. Running capture --install again replaces the local machine profile after a browser update.

compare

Measure an installed browser and the selected client profile against the same local server, then print a field-by-field diff.

The browser is measured over both HTTP/1.1 and HTTP/2. HTTP/1.1 names are compared with their exact wire casing; HTTP/2 remains lower-case as required by the protocol. Profiles that predate the optional HTTP/1.1 section continue to work with the historical fallback.

tls-forge compare
tls-forge compare --profile chrome_151 --headless
tls-forge compare --json > comparison.json

Usage: tls-forge compare [flags]

Flag Meaning
-p, --profile <name-or-path> Profile to check; default chrome
-b, --browser <name-or-path> Browser to measure; auto-detected when omitted
--headless Run the browser without a window
-j, --json Print both captures and the comparison as JSON
-c, --color <mode> auto, always or never; default auto
-f, --full Do not trim matching values; differences are always printed in full
-t, --timeout <duration> Maximum browser measurement time; default 2m
--browser-arg <argument> Append a browser process argument; repeatable

Exit codes are meaningful:

Code Meaning
0 Browser and client match on every compared field
3 Measurement succeeded, but fingerprints differ
1 Comparison could not be completed
2 Invalid arguments or help output
profiles

List profiles from the local profile directory, profiles embedded in this binary and compatible profiles from the underlying transport catalogue.

tls-forge profiles

Usage: tls-forge profiles. This command has no flags.

Profile names can be passed to --profile. A JSON profile path is also accepted directly:

tls-forge fetch --profile ./profiles/my-chrome.json https://example.com/
proxy

Run an intercepting HTTP/HTTPS proxy. It terminates incoming TLS and creates a new outbound connection with the selected browser fingerprint. A normal CONNECT tunnel cannot replace a fingerprint because it passes the caller's TLS bytes through unchanged.

tls-forge proxy
export HTTPS_PROXY=http://127.0.0.1:8080
curl --proxy http://127.0.0.1:8080 \
  --cacert "/path/printed/by/tls-forge" https://example.com/

The command prints the exact certificate path. Its default follows the OS user config directory: usually ~/.config/tls-forge/ca.pem on Linux, ~/Library/Application Support/tls-forge/ca.pem on macOS, and %AppData%\tls-forge\ca.pem on Windows.

Usage: tls-forge proxy [flags]

Flag Meaning
-p, --profile <name-or-path> Profile used for outbound requests
-x, --proxy <url> Optional upstream proxy
-t, --timeout <duration> Outbound request timeout; default 30s
-k, --insecure Skip certificate verification on outbound connections
-a, --addr <host:port> Listen address; default 127.0.0.1:8080
--ca-cert <file> CA certificate; generated in the user config directory by default
--ca-key <file> Matching CA private key
-q, --quiet Suppress per-connection error messages
--max-request-body <bytes> Largest request body buffered by the proxy; default 16777216 (16 MiB)

The proxy deliberately has no cookie jar; it forwards the caller's Cookie header. The generated CA can impersonate any site to a client that trusts it. Prefer trusting it only for the specific process, keep the private key secret, and remove that trust when the proxy is no longer needed. The proxy does not authenticate incoming clients: a non-loopback --addr must be protected by a firewall or authenticated tunnel. The certificate and key must either both exist and match or both be absent. Incoming connections expire after 30 seconds without any read or write progress, and the generated leaf-certificate cache is limited to 1024 hostnames.

serve

Run the local HTTPS measurement server independently. / contains the browser capture page; /api/all returns the current connection's TLS and HTTP fingerprint as JSON.

tls-forge serve
tls-forge serve --addr 0.0.0.0:8443
curl -k https://localhost:8443/api/all

Usage: tls-forge serve [flags]

Flag Meaning
-a, --addr <host:port> Listen address; default 127.0.0.1:0 chooses an available port
--host <hostname> Hostname used in the URL, certificate and SNI; default localhost
--session-tickets Enable TLS session resumption; disabled by default because resumption changes the fingerprint

The certificate is generated for each run, so test clients must trust it or explicitly disable verification for this local endpoint. The server does not authenticate incoming clients; the CLI warns when --addr exposes it beyond loopback. A connection that makes no read or write progress for 30 seconds is closed.

daemon

Run one long-lived client behind a JSON Lines protocol. This is the transport used by the Node.js and Python SDKs.

echo '{"id":1,"url":"https://example.com/"}' | tls-forge daemon

Usage: tls-forge daemon [flags]

Flag Meaning
-b, --cookie name=value Seed one cookie; repeatable
--cookies <file> Load a warmed session
--cookie-set <id> Select a set from the cookie file
-p, --profile <name-or-path> Profile to impersonate
-x, --proxy <url> Upstream proxy
-t, --timeout <duration> Transport request timeout; default 30s
-k, --insecure Skip certificate verification

One request object is read per line:

{"id":1,"method":"POST","url":"https://example.com/api","headers":{"content-type":"application/json"},"order":["content-type"],"body":"{\"a\":1}","setCookie":["session=abc"]}

The response repeats id and contains status, final url, body, multi-valued headers, cookies, and error when the request failed. Input lines are limited to 16 MiB. Requests are processed sequentially so one daemon remains one browser identity and one cookie jar.

version

Print the version stamped into the binary:

tls-forge version

Usage: tls-forge version. This command has no flags. A source build without version linker flags reports dev.

Language libraries

The root README intentionally contains only a working introduction for each SDK. Every language has its own detailed API document with all constructor and request options, response fields, lifecycle rules and failure behaviour.

Go
go get github.com/Sec-CH-Lemon/tls-forge
package main

import (
	"fmt"
	"log"

	tlsforge "github.com/Sec-CH-Lemon/tls-forge"
)

func main() {
	client, err := tlsforge.New(tlsforge.WithProfile("chrome"))
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	res, err := client.Get("https://tls.browserleaks.com/json")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Status, res.Text())
}

See the complete Go library documentation. The runnable consumer project is in example/go/.

Node.js
npm install tls-forge
import { Client } from 'tls-forge';

const client = new Client({ profile: 'chrome' });
try {
  const res = await client.get('https://tls.browserleaks.com/json');
  console.log(res.status, res.body);
} finally {
  client.close();
}

See the complete Node.js SDK documentation. The runnable consumer project is in example/node/.

Python
pip install tls-forge
import tlsforge

with tlsforge.Client(profile="chrome") as client:
    response = client.get("https://tls.browserleaks.com/json")
    print(response.status, response.json())

See the complete Python SDK documentation. The runnable consumer project is in example/python/.

Development

make test         # Go race tests
make cover        # 100% Go statement coverage gate
make node-test    # 100% Node line/function/branch coverage gate
make python-test  # 100% Python statement/branch coverage gate
make wrapper-smoke # Node and Python against the real Go daemon
make vet
make lint
make check        # all of the above

Release and maintenance documentation:

Licence and attribution

The project is licensed under the Apache License 2.0. Redistributed source or binaries must retain the required licence and attribution notices described by that licence.

  • NOTICE contains the project's copyright and attribution notice.
  • THIRD-PARTY-NOTICES.txt contains licences and notices for dependencies linked into distributed binaries.

The captured ClientHello and browser profile fixtures are measurements of Chrome's protocol output; they do not contain Chrome source code. The project is not affiliated with or endorsed by Google or Cloudflare.

Use the software only on systems and traffic you are authorised to access.

Documentation

Overview

Package tlsforge is an HTTP client whose TLS and HTTP/2 fingerprints are a real browser's.

The problem it solves is narrow and specific. Go's crypto/tls — like Node's OpenSSL binding, like Python's ssl — offers no control over extension order, GREASE values or the extension set, and those are exactly what JA3 and JA4 hash. Chrome uses BoringSSL and sends something no stock TLS stack can produce. A request that claims to be Chrome in its User-Agent and is demonstrably not Chrome in its handshake is not a slightly imperfect disguise; it is a contradiction, and it is trivially detectable.

This package sends the browser's bytes. The profiles are not written by hand from documentation: they are captured from a browser running on your own machine (`tls-forge capture`) and verified against it (`tls-forge compare`), which is why the claim can be checked rather than believed.

client, err := tlsforge.New(tlsforge.WithProfile("chrome"))
if err != nil { return err }
defer client.Close()

res, err := client.Get("https://tls.browserleaks.com/json")

Scope

This is a network-layer tool. It makes a request look like it came from a browser; it does not run JavaScript, execute challenges, or solve CAPTCHAs. Use it where you are permitted to make the requests you are making.

Index

Constants

View Source
const DefaultMaxResponseBody int64 = 64 << 20

DefaultMaxResponseBody bounds the decompressed body retained in memory.

View Source
const DefaultProfile = "chrome"

DefaultProfile is used when no profile is named. It is a Chrome measured from a real browser, not an approximation of one.

View Source
const Timeout = 30 * time.Second

Timeout is the default request deadline.

Variables

View Source
var ErrResponseTooLarge = errors.New("tlsforge: response body exceeds the configured limit")

ErrResponseTooLarge reports that a decompressed response exceeded its configured in-memory limit.

Functions

func MeasureBrowser

func MeasureBrowser(ctx context.Context, opts MeasureOptions) (*capture.Capture, error)

MeasureBrowser launches a browser, points it at a local server and returns what it sent.

The window that opens shows the result and can be closed; the browser runs with a throwaway profile directory, so nothing touches the user's own.

func MeasureBrowserAt

func MeasureBrowserAt(ctx context.Context, server *echo.Server, opts MeasureOptions) (*capture.Capture, error)

MeasureBrowserAt measures a browser against a server the caller already has, which is how a comparison puts both sides on one instrument.

func MeasureSelf

func MeasureSelf(ctx context.Context, opts ...Option) (*capture.Capture, error)

MeasureSelf returns what THIS library sends, measured the same way.

func MeasureSelfAt

func MeasureSelfAt(ctx context.Context, server *echo.Server, opts ...Option) (*capture.Capture, error)

MeasureSelfAt measures this library against a server the caller already has.

Types

type Client

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

Client is an HTTP client wearing one browser's fingerprint.

One client is one identity: one TLS fingerprint, one cookie jar, one exit IP. Rotating any of those means a new client, which is deliberate — a jar shared between two fingerprints describes a browser that changed its TLS stack mid-session.

A Client is safe for concurrent use.

func New

func New(opts ...Option) (*Client, error)

New builds a client. Without options it uses the default profile.

func (*Client) Close

func (c *Client) Close() error

Close releases the client's connections. A closed client must not be reused.

func (*Client) Cookies

func (c *Client) Cookies(rawURL string) ([]Cookie, error)

Cookies returns the jar's cookies for a URL.

func (*Client) CookiesFor

func (c *Client) CookiesFor(rawURL string) ([]Cookie, error)

CookiesFor returns what the jar holds for a URL, which is how a warmed session is read back out and written down.

func (*Client) Do

func (c *Client) Do(req *Request) (*Response, error)

Do performs a request.

func (*Client) Get

func (c *Client) Get(url string) (*Response, error)

Get fetches a URL with the profile's headers.

func (*Client) Headers

func (c *Client) Headers() Header

Headers returns the default headers, in order.

func (*Client) Profile

func (c *Client) Profile() *profile.Profile

Profile returns the profile this client wears.

type Comparison

type Comparison struct {
	Browser *capture.Capture `json:"browser"`
	Client  *capture.Capture `json:"client"`
	TLS     fingerprint.Report
	HTTP1   fingerprint.Report
	HTTP2   fingerprint.Report
}

Comparison is the result of measuring both sides.

func Compare

func Compare(browserCapture, clientCapture *capture.Capture) (*Comparison, error)

Compare diffs two measurements that were taken earlier, which is how a capture saved to disk is checked against today's client.

func CompareToBrowser

func CompareToBrowser(ctx context.Context, measure MeasureOptions, opts ...Option) (*Comparison, error)

CompareToBrowser measures a browser and this library on one server and diffs them.

This is the function behind `tls-forge compare`, and the answer to "is the impersonation still good?" after a browser update — which is when it silently stops being true.

func (*Comparison) OK

func (c *Comparison) OK() bool

OK reports a client indistinguishable from the browser on every field compared.

func (*Comparison) String

func (c *Comparison) String() string
type Cookie struct {
	Name     string
	Value    string
	Domain   string
	Path     string
	Secure   bool
	HTTPOnly bool
	Expires  time.Time
}

Cookie is one cookie to seed the jar with.

Domain, Secure and HttpOnly matter for a session warmed elsewhere: a cookie the server set for a parent domain has to go back to the whole of it, and one marked Secure has to keep saying so. An empty Domain means the host being asked, which is what a cookie given as a bare name and value means.

type Header []profile.Field

Header is an ordered list of headers.

A list rather than a map because header order is fingerprinted. Go's net/http.Header is a map, and a map has no order — which is why libraries built on it emit headers alphabetically or at random, and why a client with a perfect TLS handshake can still be spotted from its first request.

func NewHeader

func NewHeader(pairs ...string) Header

NewHeader builds a header from alternating name/value pairs. An odd number of arguments drops the last one rather than panicking: this is convenience syntax, not a place to lose a request over.

func (*Header) Add

func (h *Header) Add(name, value string)

Add appends another value for a header. Unlike Set it deliberately keeps an existing value; repeated Cookie, Via and X-Forwarded-For fields are distinct fields on the wire and must not be collapsed by the proxy.

func (Header) Clone

func (h Header) Clone() Header

Clone returns an independent copy.

func (*Header) Del

func (h *Header) Del(name string)

Del removes a header.

func (Header) Get

func (h Header) Get(name string) string

Get returns the first value for a name, matched case-insensitively.

func (Header) Has

func (h Header) Has(name string) bool

Has reports whether a name is present, including with an empty value.

func (Header) Merge

func (h Header) Merge(overrides Header) Header

Merge layers overrides onto a base, keeping the base's ordering for names it already has and appending the rest.

This is how a per-request header meets a browser profile: a caller setting `referer` gets the browser's order with referer in the browser's slot, not a browser-shaped list with one header bolted onto the end.

func (Header) Names

func (h Header) Names() []string

Names lists the header names in order.

func (*Header) Set

func (h *Header) Set(name, value string)

Set replaces a header IN PLACE, keeping its position, or appends it.

Keeping the position is the point. A Set that deleted and re-appended would move the header to the end of the list and change the fingerprint, so overriding one value of a browser profile would silently stop looking like that browser.

func (Header) Values

func (h Header) Values(name string) []string

Values returns every value for a name, in wire order.

type MeasureOptions

type MeasureOptions struct {
	// Browser names an installed browser ("chrome", "chromium", "edge",
	// "brave") or is a path to an executable. Empty searches for one.
	Browser string

	// Headless runs without a window.
	//
	// Chrome's modern headless was measured to send the identical handshake:
	// same JA4, same HTTP/2 fingerprint, same header order. It is still not the
	// default, because the browser being impersonated is a headed one, and
	// "identical when last measured" is a fact about the past.
	Headless bool

	// Timeout bounds the whole measurement. Zero means two minutes, which is
	// generous on purpose: it includes a cold browser start.
	Timeout time.Duration

	// BrowserArgs are extra flags for the browser, appended last so they win.
	//
	// For the machine rather than for the measurement. `--no-sandbox` is the one
	// a CI runner needs: Ubuntu 24.04 restricts the unprivileged user namespaces
	// Chrome's sandbox is built on, and a Windows runner denies the sandbox
	// access to the executable in its tool cache. In both, Chrome starts, never
	// loads the page, and the measurement times out having launched a browser
	// that was never going to answer. Process isolation is not TLS, so the
	// capture is the same capture — but nothing here checks that, and a flag
	// that did change what goes on the wire would quietly make this a
	// measurement of something else.
	BrowserArgs []string
}

MeasureOptions controls a browser measurement.

type Option

type Option func(*config)

Option configures a Client.

func WithCookieJar

func WithCookieJar(jar fhttp.CookieJar) Option

WithCookieJar supplies a jar, which is how a session is shared between clients or restored from disk.

func WithCookies

func WithCookies(cookies []Cookie) Option

WithCookies starts a client with a session already warmed.

On the client rather than on a request, because a jar is an identity: two warmed sessions seeded into one client describe a browser that was two people at once.

func WithFixedExtensionOrder

func WithFixedExtensionOrder() Option

WithFixedExtensionOrder stops shuffling the TLS extension order.

Shuffling is on by default because Chrome shuffles. Turn it off to impersonate a client that does NOT — Firefox and Safari send a stable order, and against those a shuffling client is the anomaly.

func WithHeaders

func WithHeaders(h Header) Option

WithHeaders layers default headers over the profile's, for every request.

func WithInsecureSkipVerify

func WithInsecureSkipVerify() Option

WithInsecureSkipVerify disables certificate verification.

This exists for one honest use: talking to the local echo server, whose certificate is generated per run and signs nothing. Anywhere else it removes the guarantee that you are talking to who you think you are — and a tool for looking like a browser has no business being easier to intercept than one.

func WithMaxResponseBody

func WithMaxResponseBody(bytes int64) Option

WithMaxResponseBody limits the decompressed response retained in memory. The client returns ErrResponseTooLarge when the limit is exceeded.

func WithProfile

func WithProfile(name string) Option

WithProfile selects a profile by name — a measured one such as "chrome", or any entry from the tls-client catalogue. See profile.Names.

func WithProfileValue

func WithProfileValue(p *profile.Profile) Option

WithProfileValue uses a profile directly, which is how a freshly captured one is used without registering it.

func WithProxy

func WithProxy(proxyURL string) Option

WithProxy routes requests through a proxy: http://, https://, socks5:// or socks5h://, with optional user:pass credentials.

func WithRandomExtensionOrder

func WithRandomExtensionOrder() Option

WithRandomExtensionOrder explicitly enables per-connection extension shuffling. Profiles select the browser's behaviour automatically; this is for a custom Chromium profile whose name and user agent do not reveal its family.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the per-request deadline.

func WithTransportOption

func WithTransportOption(opts ...tls_client.HttpClientOption) Option

WithTransportOption passes an option through to the underlying tls-client, for the cases this API does not cover.

An escape hatch, on purpose: the alternative is either a wrapper for every option the dependency has — a list that goes stale — or a hard stop for callers who need one of them.

func WithoutCookieJar

func WithoutCookieJar() Option

WithoutCookieJar stops the client from keeping cookies of its own.

One case needs this and it is not an optimisation. A proxy forwards whatever Cookie header its caller sent, and a jar underneath would add a second one from its own store, leaving the caller's session and the proxy's quietly diverging. Whoever holds the session should be the only one holding it.

func WithoutRedirects

func WithoutRedirects() Option

WithoutRedirects returns the 3xx instead of following it.

type Request

type Request struct {
	// Context cancels this request. Nil means context.Background.
	Context context.Context
	// Method defaults to GET.
	Method string
	URL    string
	// Header is layered over the profile's headers, keeping the profile's order
	// for names it already has.
	Header Header
	Body   []byte
	// Cookies are added to the jar before the request.
	//
	// Through the jar rather than through a Cookie header on purpose: setting
	// the header by hand REPLACES whatever the jar holds, so cookies the server
	// set earlier in the session would silently vanish from this request — which
	// no real browser would ever do.
	Cookies []Cookie
}

Request is one HTTP request.

type Response

type Response struct {
	Status int
	// URL is the final URL, after redirects.
	URL     string
	Header  map[string][]string
	Body    []byte
	Cookies []string
}

Response is one HTTP response, with the body already read and decompressed.

func (*Response) OK

func (r *Response) OK() bool

OK reports a 2xx status.

func (*Response) Text

func (r *Response) Text() string

Text returns the body as a string.

Directories

Path Synopsis
Package browser finds and launches a locally installed Chromium browser.
Package browser finds and launches a locally installed Chromium browser.
Package capture is the exchange format between a client that connects and the report describing what it sent.
Package capture is the exchange format between a client that connects and the report describing what it sent.
cmd
tls-forge command
Command tls-forge fetches URLs with a browser's TLS fingerprint, and — more usefully — proves that it does.
Command tls-forge fetches URLs with a browser's TLS fingerprint, and — more usefully — proves that it does.
Package cookie is the file a warmed session is kept in.
Package cookie is the file a warmed session is kept in.
Package daemon speaks JSON lines on a pipe, so a program in any language can borrow a browser's fingerprint without reimplementing one.
Package daemon speaks JSON lines on a pipe, so a program in any language can borrow a browser's fingerprint without reimplementing one.
Package echo is a local HTTPS server that tells every client what it just sent.
Package echo is a local HTTPS server that tells every client what it just sent.
Package fingerprint turns the bytes a TLS client actually put on the wire into the hashes a fingerprinting service would compute from them.
Package fingerprint turns the bytes a TLS client actually put on the wire into the hashes a fingerprinting service would compute from them.
internal
atomicfile
Package atomicfile replaces files without exposing partially written data.
Package atomicfile replaces files without exposing partially written data.
deadlineconn
Package deadlineconn applies a rolling idle timeout to a network connection.
Package deadlineconn applies a rolling idle timeout to a network connection.
Package profile describes one browser build completely enough to impersonate it: the ClientHello, the HTTP/2 preamble, and the headers with their order.
Package profile describes one browser build completely enough to impersonate it: the ClientHello, the HTTP/2 preamble, and the headers with their order.
Package proxy is an HTTP proxy that re-originates every request with a browser's TLS fingerprint.
Package proxy is an HTTP proxy that re-originates every request with a browser's TLS fingerprint.

Jump to

Keyboard shortcuts

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