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
- Variables
- func MeasureBrowser(ctx context.Context, opts MeasureOptions) (*capture.Capture, error)
- func MeasureBrowserAt(ctx context.Context, server *echo.Server, opts MeasureOptions) (*capture.Capture, error)
- func MeasureSelf(ctx context.Context, opts ...Option) (*capture.Capture, error)
- func MeasureSelfAt(ctx context.Context, server *echo.Server, opts ...Option) (*capture.Capture, error)
- type Client
- func (c *Client) Close() error
- func (c *Client) Cookies(rawURL string) ([]Cookie, error)
- func (c *Client) CookiesFor(rawURL string) ([]Cookie, error)
- func (c *Client) Do(req *Request) (*Response, error)
- func (c *Client) Get(url string) (*Response, error)
- func (c *Client) Headers() Header
- func (c *Client) Profile() *profile.Profile
- type Comparison
- type Cookie
- type Header
- func (h *Header) Add(name, value string)
- func (h Header) Clone() Header
- func (h *Header) Del(name string)
- func (h Header) Get(name string) string
- func (h Header) Has(name string) bool
- func (h Header) Merge(overrides Header) Header
- func (h Header) Names() []string
- func (h *Header) Set(name, value string)
- func (h Header) Values(name string) []string
- type MeasureOptions
- type Option
- func WithCookieJar(jar fhttp.CookieJar) Option
- func WithCookies(cookies []Cookie) Option
- func WithFixedExtensionOrder() Option
- func WithHeaders(h Header) Option
- func WithInsecureSkipVerify() Option
- func WithMaxResponseBody(bytes int64) Option
- func WithProfile(name string) Option
- func WithProfileValue(p *profile.Profile) Option
- func WithProxy(proxyURL string) Option
- func WithRandomExtensionOrder() Option
- func WithTimeout(d time.Duration) Option
- func WithTransportOption(opts ...tls_client.HttpClientOption) Option
- func WithoutCookieJar() Option
- func WithoutRedirects() Option
- type Request
- type Response
Constants ¶
const DefaultMaxResponseBody int64 = 64 << 20
DefaultMaxResponseBody bounds the decompressed body retained in memory.
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.
const Timeout = 30 * time.Second
Timeout is the default request deadline.
Variables ¶
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 ¶
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 ¶
MeasureSelf returns what THIS library sends, measured the same way.
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 (*Client) CookiesFor ¶
CookiesFor returns what the jar holds for a URL, which is how a warmed session is read back out and written down.
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 ¶
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 ¶
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 ¶
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 ¶
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) Merge ¶
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) Set ¶
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.
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 ¶
WithCookieJar supplies a jar, which is how a session is shared between clients or restored from disk.
func WithCookies ¶
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 ¶
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 ¶
WithMaxResponseBody limits the decompressed response retained in memory. The client returns ErrResponseTooLarge when the limit is exceeded.
func WithProfile ¶
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 ¶
WithProfileValue uses a profile directly, which is how a freshly captured one is used without registering it.
func WithProxy ¶
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 ¶
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.
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. |