signals

package
v1.12.0 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package signals tells people from bots and recognizes devices, from what the server sees of each request: its TLS ClientHello (JA4), its HTTP/2 settings and the order of its headers, how its headers fit the browser its User-Agent claims, and, optionally, a client check in the browser with a proof of work. It is for an app's own security: blocking abuse, limiting rates per client, noticing a sign-in from a new device. It doesn't follow people across sites, and it keeps nothing.

sig := signals.New(signals.Options{Check: true})
ternApp(kit.WithPlugins(sig))

func Actions… {
	if s := signals.Of(c); s.Verdict == signals.Bot { … }
}

The TLS and HTTP/2 fingerprints need the App to terminate TLS itself (kit.WithAutoTLS, kit.WithTLS). Behind a proxy that terminates it, the proxy can pass its JA4 on in a header (Options.JA4Header); the rest of the signals work anywhere.

Index

Constants

View Source
const (
	Human      = "human"
	Suspicious = "suspicious"
	Bot        = "bot"
)

Verdicts.

Variables

This section is empty.

Functions

func Key

func Key(r *http.Request) string

Key identifies the client of r for rate limits: its fingerprints and IP address, so clients that share an address are told apart and a client that hops addresses is not. Without the plugin it is kit.ClientIP.

ratelimit.New(ratelimit.KeyFunc(signals.Key))

Types

type Client

type Client struct {
	// Browser is Chrome, Edge, Opera, Samsung Internet, Firefox or Safari,
	// or "" for anything else.
	Browser string `json:"browser,omitempty"`
	// Version is the browser's major version.
	Version int `json:"version,omitempty"`
	// OS is Windows, macOS, iOS, Android, Linux or ChromeOS, or "".
	OS     string `json:"os,omitempty"`
	Mobile bool   `json:"mobile,omitempty"`
	// Crawler names a search engine or other crawler that says it is one,
	// such as Googlebot. It says nothing of whether it's true.
	Crawler string `json:"crawler,omitempty"`
	// Tool names an HTTP library or automation tool, such as curl,
	// python-requests or HeadlessChrome.
	Tool string `json:"tool,omitempty"`
}

Client is what a request's User-Agent says it is.

func (Client) Chromium

func (c Client) Chromium() bool

Chromium reports whether the browser is built on Chromium.

type Options

type Options struct {
	// Check adds the client check to every page: a script that solves a
	// small proof of work and looks for signs of automation, once a day
	// per browser. Signals.Checked tells who passed.
	Check bool
	// Difficulty is the check's proof of work, in leading zero bits of a
	// SHA-256. Each bit doubles the work; the default, 16, takes a phone
	// a fraction of a second.
	Difficulty int
	// Secret signs the check's challenges and results. The default is
	// TERN_SECRET's; without either, a random one is made at start, and
	// browsers check again after a restart.
	Secret string

	// Block answers requests that change things (POST, PUT, PATCH,
	// DELETE) with 403 when their score is Block or more. 0 blocks
	// nothing; the app decides with Of.
	Block int

	// JA4Header names a header with the client's JA4 fingerprint, set by
	// a proxy that terminates TLS in front of the App, such as
	// Cloudflare's. It is read only from proxies the App trusts
	// (kit.WithTrustedProxies).
	JA4Header string

	// DeviceCookie names the cookie that identifies a device. The default
	// is "tern_device".
	DeviceCookie string
}

Options configures the plugin.

type Plugin

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

Plugin computes the signals. Make it with New.

func New

func New(o Options) *Plugin

New returns the plugin.

func (*Plugin) Name

func (*Plugin) Name() string

Name implements kit.Plugin.

func (*Plugin) Setup

func (p *Plugin) Setup(s *kit.Setup) error

Setup implements kit.Plugin.

type Signals

type Signals struct {
	// Score is 0 for a request that looks like a person's browser, up to
	// 100 for one that is plainly automated.
	Score int `json:"score"`
	// Verdict is Human (score under 30), Suspicious (under 70) or Bot.
	Verdict string `json:"verdict"`
	// Reasons are codes for what raised or lowered the score, such as
	// "tool:curl" or "tls-no-grease"; docs/signals.md lists them.
	Reasons []string `json:"reasons,omitempty"`

	// Client is what the User-Agent claims.
	Client Client `json:"client"`
	// JA4 is the TLS client fingerprint, when known.
	JA4 string `json:"ja4,omitempty"`
	// HTTP2 is the HTTP/2 fingerprint (SETTINGS, WINDOW_UPDATE, PRIORITY
	// and pseudo-header order), for HTTP/2 requests to the App's own TLS.
	HTTP2 string `json:"http2,omitempty"`
	// Headers fingerprints the request's headers: JA4H's first two parts,
	// from the method, version, header count, language and the header
	// names in the order sent.
	Headers string `json:"headers,omitempty"`
	// Proto is the HTTP version.
	Proto string `json:"proto"`

	// Checked reports that this browser passed the client check
	// (Options.Check) within the day.
	Checked bool `json:"checked"`

	// Device identifies the browser, from a cookie, with Of; NewDevice
	// reports that the cookie was set by this request.
	Device    string `json:"device,omitempty"`
	NewDevice bool   `json:"newDevice,omitempty"`
}

Signals are what the plugin found of a request.

func From

func From(r *http.Request) *Signals

From returns the signals of r, without the device. It returns nil if the plugin isn't installed.

func Of

func Of(c *kit.Ctx) *Signals

Of returns the signals of c's request, and its device: the device cookie is set if the browser has none. It returns nil if the plugin isn't installed.

Jump to

Keyboard shortcuts

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