Documentation
¶
Overview ¶
Package uax turns a request into a description of the client behind it.
It reads the User-Agent string, the Client Hints headers, and the fetch and content-negotiation metadata that travel with them, and reports the browser, operating system, device, bot, in-app WebView, automation framework or HTTP library responsible — along with how confident it is, whether the client's claims contradict each other, and what it suggests doing about that.
Getting started ¶
c := uax.Parse(r.UserAgent()) fmt.Println(c.Browser.Name, c.OS.Name, c.Device.Name)
Prefer ParseRequest wherever the whole request is available. Client Hints are structured, are what browser vendors intend servers to read, and survive the User-Agent reduction that is steadily emptying the legacy string:
c := uax.ParseRequest(r)
if c.IsAICrawler() { … }
Configuration ¶
The package-level functions are pure: no cache, no recorder, no shared state. A server that wants caching, metrics or custom rules builds its own parser:
p := uax.New(uax.Config{
Cache: uax.CacheConfig{Enabled: true},
Metrics: myReporter,
})
What this package will not do ¶
It never invents facts. A device model that is not in the database yields an empty Device.Name rather than a guess, because a wrong device name is worse than none. Predicates require positive evidence: an unrecognised client is unknown, not a desktop by elimination. And everything a client says about itself is a claim — Bot.Name is what the client typed, not proof of who sent the request.
Rules ¶
Classification rules live in internal/table, in Go. They are reviewed as code, and a set of lint tests holds the line that once belonged to a generator: every rule has a unique ID within its category, exactly one match clause, a priority, a confidence in (0,1], and at least one test proving it. A rule without a test fails the build.
Devices ¶
Naming hardware is a catalogue problem, not a parsing one — "SM-S928B" does not contain "Galaxy S24 Ultra", and no rule can derive it — and catalogues move on their own schedule, since phones ship weekly and parsers do not. So this package holds no device names. It extracts the model code a request carries and hands it to whatever the caller configured:
import "github.com/bakhod1r/devicex"
p := uax.New(uax.Config{Device: devicex.Describe})
Without Config.Device, Device.Model still reports the code the request carried. Only the marketing name, brand and form factor go unanswered, which is the honest result of having nothing to answer them with.
Index ¶
- Constants
- func DatabaseRules() int
- func DatabaseVersion() string
- func ParseTo(dst *Client, raw string)
- func Redact(token string) string
- type AI
- type AcceptProfile
- type Action
- type App
- type Automation
- type Bot
- type BotCategory
- type BrandHint
- type Browser
- type Cache
- type CacheConfig
- type CategorySummary
- type Client
- func (c *Client) Attrs() []slog.Attr
- func (c *Client) Coarse() *Client
- func (c *Client) Consistency() Consistency
- func (c *Client) IsAICrawler() bool
- func (c *Client) IsAndroid() bool
- func (c *Client) IsAutomated() bool
- func (c *Client) IsBot() bool
- func (c *Client) IsCLI() bool
- func (c *Client) IsConsole() bool
- func (c *Client) IsDesktop() bool
- func (c *Client) IsHeadless() bool
- func (c *Client) IsIOS() bool
- func (c *Client) IsLinux() bool
- func (c *Client) IsMacOS() bool
- func (c *Client) IsMobile() bool
- func (c *Client) IsSuspicious() bool
- func (c *Client) IsTV() bool
- func (c *Client) IsTablet() bool
- func (c *Client) IsTool() bool
- func (c *Client) IsWebView() bool
- func (c *Client) IsWindows() bool
- func (c *Client) LogValue() slog.Value
- func (c *Client) Privacy() Privacy
- func (c *Client) ToJSON() []byte
- type Config
- type Consistency
- type Device
- type DeviceType
- type Engine
- type FetchContext
- type Finding
- type HTTPClient
- type Hints
- type Kind
- type MetricsReporter
- type NoopReporter
- type OS
- type Parser
- type Privacy
- type PrivacyRisk
- type Reason
- type Recorder
- type Registry
- func (r *Registry) RegisterApp(rules ...Rule)
- func (r *Registry) RegisterAutomation(rules ...Rule)
- func (r *Registry) RegisterBot(rules ...Rule)
- func (r *Registry) RegisterBrowser(rules ...Rule)
- func (r *Registry) RegisterEngine(rules ...Rule)
- func (r *Registry) RegisterHTTPClient(rules ...Rule)
- func (r *Registry) RegisterOS(rules ...Rule)
- func (r *Registry) Reset()
- type Rule
- type Severity
- type Source
- type Unknown
- type Verdict
- type Version
Constants ¶
const ( // DefaultMaxInputLength is a generous bound on a real User-Agent, which // rarely exceeds 512 bytes. It exists to cap the work a hostile client can // cause, not to reject unusual browsers. DefaultMaxInputLength = 4096 // DefaultCacheSize applies when caching is enabled without a size. DefaultCacheSize = 4096 )
const ( HeaderCHUA = "Sec-CH-UA" HeaderCHUAMobile = "Sec-CH-UA-Mobile" HeaderCHUAPlatform = "Sec-CH-UA-Platform" HeaderCHUAPlatformVersion = "Sec-CH-UA-Platform-Version" HeaderCHUAModel = "Sec-CH-UA-Model" HeaderCHUAArch = "Sec-CH-UA-Arch" HeaderCHUABitness = "Sec-CH-UA-Bitness" HeaderCHUAWoW64 = "Sec-CH-UA-WoW64" HeaderCHUAFullVersionList = "Sec-CH-UA-Full-Version-List" HeaderCHUAFormFactors = "Sec-CH-UA-Form-Factors" )
Header names for every hint this package understands.
const AcceptCH = "" /* 131-byte string literal not displayed */
AcceptCH is the header value asking a browser to send the high-entropy hints this package can use. Serve it as `Accept-CH: <AcceptCH>` to receive the OS version and device model that a modern User-Agent no longer contains.
const DefaultRecorderLimit = 4096
DefaultRecorderLimit bounds how many distinct shapes a recorder retains. Without a bound, a hostile client could grow the map without limit simply by sending a novel token on every request.
const Release = "0.1.0"
Release is the version of this library, matching its Git tag.
It is a constant rather than build-time linker flags so that `go get` of a tagged module reports the same value however it was built.
Variables ¶
This section is empty.
Functions ¶
func DatabaseVersion ¶
func DatabaseVersion() string
DatabaseVersion is a fingerprint of the compiled rule database.
A classification is only reproducible if you know what it was classified against, and the rules change more often than the code does. Logging this alongside a verdict makes a decision from six months ago explicable: two runs that report the same fingerprint saw the same rules.
It is derived from the rules themselves rather than maintained by hand, so it cannot be forgotten. The value is stable for a given database and carries no meaning beyond equality — it is not ordered, and a difference says the rules changed without saying how.
func ParseTo ¶
ParseTo parses into a caller-supplied Client, allocating nothing. Use it on hot paths that can reuse a Client between requests; Parse is the ordinary entry point.
func Redact ¶
Redact reduces a token to its shape: digit runs become "N", and long opaque identifiers become "X".
"MyApp/3.14.159" -> "MyApp/N.N.N" "Build/a91f0c2e77b14" -> "Build/X"
The point is to keep what identifies a kind of client while dropping what could identify an installation, so the result is safe to log, aggregate and share upstream as a rule request.
Types ¶
type AI ¶
type AI struct {
Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"`
Category BotCategory `json:"category,omitempty"` // AICrawler or AIAgent
}
AI describes an AI system fetching the resource. It is derived from the bot rule that matched rather than stored separately, so the two can never drift.
type AcceptProfile ¶
type AcceptProfile struct {
Accept string `json:"accept,omitempty"`
AcceptLanguage string `json:"accept_language,omitempty"`
AcceptEncoding string `json:"accept_encoding,omitempty"`
}
AcceptProfile represents the client's content negotiation headers.
func ParseAcceptProfile ¶
func ParseAcceptProfile(accept, acceptLanguage, acceptEncoding string) *AcceptProfile
ParseAcceptProfile creates an AcceptProfile from the provided headers.
func (*AcceptProfile) IsMissingLanguage ¶
func (a *AcceptProfile) IsMissingLanguage() bool
IsMissingLanguage returns true if a language header was expected but not provided. Most legitimate browsers send an Accept-Language header.
func (*AcceptProfile) PreferredLanguage ¶
func (a *AcceptProfile) PreferredLanguage() string
PreferredLanguage is the base language the client most wants, as a two- or three-letter subtag: "en-GB;q=0.9, fr;q=1.0" reports "fr".
It reads the whole header rather than only its first entry, because the order a client writes its languages in is not the order it prefers them — that is what the q-value is for. Entries without one default to q=1.0, as RFC 9110 says, and ties keep the order the client wrote.
The wildcard "*" is not a language and is never returned. A malformed q-value makes that one entry unusable rather than the whole header: a client that garbles its preferences is still a client with preferences.
type Action ¶
type Action string
Action is what uax suggests doing with a client.
It is advice, never a decision. This package sees one request's headers; it cannot see rate, reputation, session history or business context, and any of those may reasonably overrule it. The caller decides.
type App ¶
type App struct {
Name string `json:"name,omitempty"`
Category string `json:"category,omitempty"`
Version Version `json:"version,omitempty"`
Confidence float32 `json:"confidence,omitempty"`
}
App is the native application hosting an embedded WebView.
type Automation ¶
type Automation struct {
Name string `json:"name,omitempty"`
Category string `json:"category,omitempty"` // Headless, Framework, Embedded
Version Version `json:"version,omitempty"`
Confidence float32 `json:"confidence,omitempty"`
}
Automation is a headless browser or a browser-automation framework.
Only self-declared markers are reported. A driver configured to hide is invisible here by design, and the absence of an Automation value is therefore never evidence that a client is human.
type Bot ¶
type Bot struct {
Name string `json:"name,omitempty"`
Category BotCategory `json:"category,omitempty"`
Provider string `json:"provider,omitempty"`
Version Version `json:"version,omitempty"`
Confidence float32 `json:"confidence,omitempty"`
}
Bot is a self-identified crawler, agent or scraper.
Every field here is a claim made by the client. A User-Agent saying "Googlebot" is not proof; the uax/verify module exists to turn that claim into a verified fact using DNS and published address ranges.
func (*Bot) IsAICrawler ¶
IsAICrawler reports whether the bot collects data for a model, either for training or for live retrieval.
func (*Bot) IsSearchEngine ¶
IsSearchEngine reports whether the bot indexes for a search product.
func (*Bot) IsSocialCrawler ¶
IsSocialCrawler reports whether the bot is building a link preview.
type BotCategory ¶
type BotCategory string
BotCategory says what a bot is for. It carries far more decision value than a bare "is a bot" boolean: a search crawler, an AI trainer and a scraper all warrant different handling.
const ( BotSearchEngine BotCategory = "SearchEngine" BotAICrawler BotCategory = "AICrawler" BotAIAgent BotCategory = "AIAgent" BotSocialCrawler BotCategory = "SocialCrawler" BotSEO BotCategory = "SEO" BotMonitoring BotCategory = "Monitoring" BotSecurity BotCategory = "Security" BotGeneric BotCategory = "Generic" )
type Browser ¶
type Browser struct {
Name string `json:"name,omitempty"`
Family string `json:"family,omitempty"`
Version Version `json:"version,omitempty"`
Mode string `json:"mode,omitempty"` // Headless, WebView, "" for a normal browser
Confidence float32 `json:"confidence,omitempty"`
Source Source `json:"source,omitempty"`
}
Browser is the resolved browser identity.
Family groups derivatives under their base (Edge, Brave and Vivaldi all have the family Chrome); Name is the specific product. Mode distinguishes a headless or WebView instance of an otherwise ordinary browser.
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache is a simple, thread-safe LRU cache for parsed Clients.
type CacheConfig ¶
type CacheConfig struct {
Enabled bool
// Size is the maximum number of entries. Zero selects DefaultCacheSize.
Size int
}
CacheConfig controls result memoisation.
type CategorySummary ¶
CategorySummary is how many rules one category holds.
func DatabaseSummary ¶
func DatabaseSummary() []CategorySummary
DatabaseSummary reports the size of the compiled rule database.
It exists so a deployment can record which database produced a given result: rules change between releases, and a classification from six months ago is only reproducible if you know what it was classified against.
type Client ¶
type Client struct {
Browser Browser `json:"browser,omitempty"`
OS OS `json:"os,omitempty"`
Device Device `json:"device,omitempty"`
Engine Engine `json:"engine,omitempty"`
Bot *Bot `json:"bot,omitempty"`
AI *AI `json:"ai,omitempty"`
App *App `json:"app,omitempty"`
Automation *Automation `json:"automation,omitempty"`
HTTPClient *HTTPClient `json:"http_client,omitempty"`
Hints *Hints `json:"client_hints,omitempty"`
Fetch *FetchContext `json:"fetch,omitempty"`
AcceptProfile *AcceptProfile `json:"accept_profile,omitempty"`
Kind Kind `json:"kind,omitempty"`
Verdict *Verdict `json:"verdict,omitempty"`
// SpoofScore is 0 when nothing contradicts itself and rises towards 1 as
// the client's claims stop fitting together. See Consistency for the
// individual findings behind the number.
SpoofScore float32 `json:"spoof_score,omitempty"`
// Raw is the input as parsed. It is the truncated form when Truncated is
// set, so it always matches what the fields above were derived from.
Raw string `json:"raw,omitempty"`
Truncated bool `json:"truncated,omitempty"`
// contains filtered or unexported fields
}
Client is everything uax could determine about one caller.
Fields that describe an identity always present in a request — browser, OS, device, engine — are values. Fields that describe something that may simply not be there are pointers, so "no bot was detected" and "a bot with empty fields" can never be confused.
func Parse ¶
Parse parses a User-Agent string.
It is a pure function: no cache, no shared state, no retention of the input. For a configured parser — caching, recording, metrics, or skipping work you do not need — use New.
func ParseRequest ¶
ParseRequest parses a request's User-Agent together with its Client Hints, fetch metadata and Accept headers.
Client Hints are authoritative where present: they are structured, and they survive the User-Agent reduction that is steadily hollowing out the legacy string. Where hints and the User-Agent disagree, both are kept and the disagreement itself is recorded as a spoofing signal.
func (*Client) Attrs ¶
Attrs returns the client as structured log attributes.
The keys are stable and namespaced, so dashboards and alerts built on them keep working across releases. Empty fields are omitted rather than logged as "": absence should not occupy a column.
func (*Client) Coarse ¶
Coarse returns a copy of the client with the narrowing details removed: exact device model and name, version components below the major, and the architecture.
Use it when the client description is going somewhere it will be retained — analytics, logs, a data warehouse. What survives is enough to answer "which browsers and platforms do my users have", which is what those systems are usually for, without carrying a per-visitor identifier along with it.
func (*Client) Consistency ¶
func (c *Client) Consistency() Consistency
Consistency returns the coherence check for this client.
func (*Client) IsAICrawler ¶
IsAICrawler reports whether an AI system is fetching, either to collect data or to answer a user's live request.
func (*Client) IsAutomated ¶
IsAutomated reports whether a browser-automation framework was declared.
func (*Client) IsBot ¶
IsBot reports whether the client identified itself as a crawler, agent or scraper. This is a claim by the client, not a verified fact.
func (*Client) IsHeadless ¶
IsHeadless reports whether the client declared itself headless.
func (*Client) IsLinux ¶
IsLinux reports whether the client runs a Linux distribution. Android is a separate platform here, as callers almost always mean desktop Linux.
func (*Client) IsSuspicious ¶
IsSuspicious reports whether anything about the client failed to add up.
func (*Client) IsWebView ¶
IsWebView reports whether the page is rendered inside an embedded browser.
func (*Client) LogValue ¶
LogValue makes Client usable directly as a slog value:
logger.Info("request", "client", client)
func (*Client) Privacy ¶
Privacy reports how identifying this client's description is.
The rating counts the specific, narrowing facts the client disclosed. An exact device model is the strongest of them: it can separate one visitor from nearly everyone else, which is precisely why Chromium stopped sending it by default and why it now requires an explicit Accept-CH request.
type Config ¶
type Config struct {
// Cache memoises results by raw User-Agent. Production traffic repeats a
// few thousand distinct strings across millions of requests, so this is
// usually worth enabling on a request path — but only when the caller has
// decided the memory ceiling is acceptable.
Cache CacheConfig
// MaxInputLength bounds how much of an input is examined. Longer input is
// truncated and flagged via Client.Truncated rather than rejected, because
// a parser that errors on a hostile string is a parser that hands the
// caller a decision it cannot make. Zero selects DefaultMaxInputLength.
MaxInputLength int
// Recorder collects unrecognised product tokens so a deployment can improve
// its own rules. It is nil by default: recording is a deliberate choice,
// never something that happens because a caller did not opt out.
Recorder *Recorder
// Metrics receives counters. Nil means no metrics are emitted.
Metrics MetricsReporter
// Registry layers custom rules over the built-in database. Nil uses the
// built-in rules alone.
Registry *Registry
// Device names the hardware behind a model code. It is nil by default, and
// then Device.Model reports the code the request carried while the name,
// brand and form factor stay empty.
//
// This package holds no device catalogue. "SM-S928B" does not contain
// "Galaxy S24 Ultra" — a manufacturer assigned that mapping, so answering
// it needs published data, and published data moves on its own schedule.
// github.com/bakhod1r/devicex is such a catalogue and satisfies this signature
// directly:
//
// p := uax.New(uax.Config{Device: devicex.Describe})
//
// The signature carries no types from either package, so neither has to
// import the other. It is called with the extracted model code and the raw
// User-Agent: the code answers Android, and the User-Agent answers the
// hardware that publishes no code and names itself in prose instead, like
// "Macintosh" or "PlayStation 5". Either argument may be empty.
//
// model is the identifier the request actually carried, which is not
// always the code passed in: hardware that publishes no code names itself
// in prose, and "iPhone" is both the name and the whole identifier. A hook
// that cannot point at something in the request returns model empty, and
// Device.Model then reports the extracted code, or nothing.
//
// deviceType is "Mobile", "Tablet", "Desktop", "Console" or empty. It must
// return ok == false rather than guess: a wrong device name is worse than
// no device name, because it is a fact-shaped value a caller will store.
Device func(code, ua string) (name, brand, model, family, deviceType string, confidence float64, ok bool)
// SkipDevice omits device resolution entirely, including the model code.
// Useful on hot paths that only need browser and OS.
SkipDevice bool
// SkipBot omits bot, AI, automation and HTTP-client resolution.
SkipBot bool
}
Config configures a Parser. The zero value is valid and is what the package-level functions use: no cache, no recorder, no shared state.
Caching is opt-in on purpose. A cache is a global mutable side effect on what is otherwise a pure function, it makes benchmarks report the cache rather than the parser, and it retains attacker-supplied strings in memory. Callers that want it should say so, and size it themselves.
type Consistency ¶
type Consistency struct {
Findings []Finding `json:"findings,omitempty"`
Score float32 `json:"score"` // 0 = coherent, 1 = impossible
}
Consistency is the result of checking a client's claims against each other.
It answers "does this client's story hold together", which is a different and more answerable question than "is this client lying". A clean result is not proof of honesty; a dirty one is strong evidence of editing.
func CheckConsistency ¶
func CheckConsistency(c *Client) Consistency
CheckConsistency evaluates a client against every impossibility rule and combines the result with any disagreement already found between the User-Agent and Client Hints.
func (Consistency) Has ¶
func (c Consistency) Has(rule string) bool
Has reports whether a specific rule fired.
func (Consistency) OK ¶
func (c Consistency) OK() bool
OK reports whether nothing contradicted anything else.
type Device ¶
type Device struct {
Type DeviceType `json:"type,omitempty"`
Brand string `json:"brand,omitempty"`
Model string `json:"model,omitempty"`
Name string `json:"name,omitempty"`
Family string `json:"family,omitempty"`
Series string `json:"series,omitempty"`
Year int `json:"year,omitempty"`
Confidence float32 `json:"confidence,omitempty"`
Source Source `json:"source,omitempty"`
// Reduced reports that the browser replaced the model code with a
// placeholder ("K"). The handset exists and has a code; this client simply
// declined to state it, which is different from a code nobody recognised.
// Sec-CH-UA-Model carries the real one and clears this flag.
Reduced bool `json:"reduced,omitempty"`
}
Device is the resolved hardware.
Model and Name are deliberately separate and must not be conflated:
Model is the verbatim code from the User-Agent ("SM-S928B"). It is evidence
and never changes.
Name is the marketing name ("Galaxy S24 Ultra"). It comes from a lookup
table, because no rule can derive one from the other. When the table has no
entry, Name stays empty — it is never approximated, since a wrong device
name is worse than none at all.
type DeviceType ¶
type DeviceType string
DeviceType is the physical form factor of the client.
const ( DeviceUnknown DeviceType = "" DeviceDesktop DeviceType = "Desktop" DeviceMobile DeviceType = "Mobile" DeviceTablet DeviceType = "Tablet" DeviceTV DeviceType = "TV" DeviceConsole DeviceType = "Console" DeviceWatch DeviceType = "Watch" DeviceCar DeviceType = "Car" DeviceXR DeviceType = "XR" // headsets and glasses that replace or augment the view DeviceEInk DeviceType = "EInk" // slow screen refresh, little or no colour DeviceEmbedded DeviceType = "Embedded" DeviceBot DeviceType = "Bot" )
type Engine ¶
type Engine struct {
Name string `json:"name,omitempty"`
Version Version `json:"version,omitempty"`
Confidence float32 `json:"confidence,omitempty"`
}
Engine is the layout engine.
type FetchContext ¶
type FetchContext struct {
Site string `json:"site,omitempty"`
Mode string `json:"mode,omitempty"`
Dest string `json:"dest,omitempty"`
User string `json:"user,omitempty"`
}
FetchContext represents the parsed Sec-Fetch-* headers.
func ParseFetchContext ¶
func ParseFetchContext(site, mode, dest, user string) *FetchContext
ParseFetchContext parses the Sec-Fetch headers.
func (*FetchContext) IsNavigation ¶
func (f *FetchContext) IsNavigation() bool
IsNavigation returns true if this fetch represents a top-level document navigation.
type Finding ¶
type Finding struct {
Rule string `json:"rule"`
Severity Severity `json:"severity"`
Detail string `json:"detail,omitempty"`
Left string `json:"left,omitempty"`
Right string `json:"right,omitempty"`
}
Finding is one internal contradiction, with the evidence behind it.
Rule is a stable identifier, safe to log, alert on and assert in tests. It never changes spelling once released, so a dashboard built on it keeps working.
type HTTPClient ¶
type HTTPClient struct {
Name string `json:"name,omitempty"`
Category string `json:"category,omitempty"` // CLI, Library, Package
Version Version `json:"version,omitempty"`
Confidence float32 `json:"confidence,omitempty"`
}
HTTPClient is a non-browser client: a CLI tool, or an HTTP library embedded in an application.
type Hints ¶
type Hints struct {
// Brands is the Sec-CH-UA list with GREASE entries removed. It contains
// significant-version numbers only ("138"), unless FullVersionList was
// requested.
Brands []BrandHint `json:"brands,omitempty"`
// FullVersionList is Sec-CH-UA-Full-Version-List: the same brands carrying
// complete versions ("138.0.7204.92"). Requires Accept-CH.
FullVersionList []BrandHint `json:"full_version_list,omitempty"`
Mobile bool `json:"mobile"`
Platform string `json:"platform,omitempty"` // "Windows", "macOS", "Android"
PlatformVersion string `json:"platform_version,omitempty"` // Requires Accept-CH
Model string `json:"model,omitempty"` // Requires Accept-CH
Arch string `json:"arch,omitempty"` // Requires Accept-CH
Bitness string `json:"bitness,omitempty"` // Requires Accept-CH
// WoW64 is Sec-CH-UA-WoW64: a 32-bit browser running on 64-bit Windows.
// It describes the process, not the machine — a WoW64 client still reports
// Bitness "64", because that is what the platform is. Requires Accept-CH.
WoW64 bool `json:"wow64,omitempty"`
// FormFactors is Sec-CH-UA-Form-Factors: what shape the device physically
// is, stated rather than inferred. Requires Accept-CH.
//
// Values are kept exactly as sent, including ones this package does not
// recognise, so a hint added to the specification after this release is
// still visible to callers.
FormFactors []string `json:"form_factors,omitempty"`
}
Hints is the parsed User-Agent Client Hints set.
Client Hints matter because the legacy User-Agent string is being frozen and hollowed out: modern Chromium reports "Android 10; K" for every phone regardless of the real OS version or model. The hints carry the truth, but only the low-entropy ones are sent by default — the rest arrive only after the server requests them with Accept-CH.
func ParseClientHints ¶
ParseClientHints parses the three low-entropy hints a browser sends without being asked. Prefer ParseHints, which reads the complete set.
func ParseHints ¶
ParseHints reads every Client Hint from a header set. It returns nil when the request carries no hints at all, so "no hints" is distinguishable from "hints with empty values".
type MetricsReporter ¶
type MetricsReporter interface {
// IncParse counts inputs actually parsed, excluding cache hits.
IncParse()
// IncCacheHit counts results served from the cache.
IncCacheHit()
// IncBotDetected counts clients that identified themselves as a bot,
// crawler or AI agent.
IncBotDetected()
// IncSpoofDetected counts clients whose claims contradict each other.
IncSpoofDetected()
}
MetricsReporter receives operational counters. Supply one via Config.Metrics to feed Prometheus, OpenTelemetry, or anything else.
The counters describe work the parser actually performed: a cache hit increments IncCacheHit and nothing else, because no parsing happened. That makes the ratio between IncParse and IncCacheHit meaningful, which is the number worth graphing.
type NoopReporter ¶
type NoopReporter struct{}
NoopReporter discards every counter. It is useful as an embedded field when implementing only some of the interface.
func (NoopReporter) IncBotDetected ¶
func (NoopReporter) IncBotDetected()
func (NoopReporter) IncCacheHit ¶
func (NoopReporter) IncCacheHit()
func (NoopReporter) IncParse ¶
func (NoopReporter) IncParse()
The bodies are empty because discarding is the whole behaviour.
func (NoopReporter) IncSpoofDetected ¶
func (NoopReporter) IncSpoofDetected()
type OS ¶
type OS struct {
Name string `json:"name,omitempty"`
Family string `json:"family,omitempty"`
Version Version `json:"version,omitempty"`
Arch string `json:"arch,omitempty"` // amd64, arm64, x86, arm — only when stated
// Bitness is the platform's CPU width, "32" or "64", exactly as
// Sec-CH-UA-Bitness stated it. It describes the machine, not the process:
// a 32-bit browser on a 64-bit Windows reports bitness 64 and sets
// Sec-CH-UA-WoW64 instead. Empty unless the client sent the hint.
Bitness string `json:"bitness,omitempty"`
Confidence float32 `json:"confidence,omitempty"`
Source Source `json:"source,omitempty"`
// Reduced reports that the platform version in the User-Agent is frozen by
// the browser rather than observed from the system. Chrome's User-Agent
// Reduction did this deliberately: every reduced Android build reports 10,
// macOS caps at 10.15.7, and Windows has reported NT 10.0 since Windows 10.
//
// When Reduced is set, Version is either absent (Android, where the value
// is a placeholder that contradicts reality) or a floor (macOS and Windows,
// where the real version is the stated one or higher). Sec-CH-UA-Platform-
// Version carries the true value and clears this flag.
Reduced bool `json:"reduced,omitempty"`
}
OS is the resolved operating system.
type Parser ¶
type Parser struct {
// contains filtered or unexported fields
}
Parser parses User-Agents under a fixed configuration.
A Parser is safe for concurrent use. The rule tables it reads are immutable after init, so parsing takes no locks; only an enabled cache synchronises, and it shards to keep contention off the hot path.
func Default ¶
func Default() *Parser
Default returns the parser backing the package-level functions: no cache, no recorder, no metrics. It is safe for concurrent use and holds no state.
func (*Parser) ParseRequest ¶
ParseRequest parses an http.Request under the parser's configuration.
type Privacy ¶
type Privacy struct {
Risk PrivacyRisk `json:"risk"`
// Identifiers lists the fields that narrow this client down, so a caller
// can see exactly what drove the rating rather than trusting a number.
Identifiers []string `json:"identifiers,omitempty"`
}
Privacy summarises how identifying a parsed client is.
type PrivacyRisk ¶
type PrivacyRisk string
PrivacyRisk is how identifying a client's self-description is.
It is a coarse ordering, not a measurement. Real fingerprinting entropy depends on the population a visitor is drawn from — a Samsung phone is unremarkable in a consumer audience and conspicuous in a fleet of managed laptops — and this package sees one request, never the population. Treat it as "how much did this client volunteer about itself", which is a question a single request can answer.
const ( PrivacyLow PrivacyRisk = "low" PrivacyMedium PrivacyRisk = "medium" PrivacyHigh PrivacyRisk = "high" )
type Reason ¶
type Reason struct {
Code string `json:"code"`
Weight float32 `json:"weight"`
Detail string `json:"detail,omitempty"`
}
Reason is one weighted input to a verdict, carrying a stable code so it can be logged, alerted on and asserted in tests.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder collects product tokens that matched no rule.
It answers the question every deployment eventually asks: what is my traffic sending that the database does not know about? Feeding those tokens back into internal/table is how the rules improve.
Two deliberate choices make it safe to run in production:
Nothing is recorded unless Config.Recorder is set. There is no default recorder, no default-on telemetry and no network access anywhere in this package — the tokens go into memory the caller owns, and nowhere else. Versions and long opaque identifiers are redacted before storage, so "MyApp/3.14.159" and a per-install build ID collapse into one shape. What is kept describes a class of client, not an individual one.
func NewRecorder ¶
func NewRecorder() *Recorder
NewRecorder returns a recorder holding at most DefaultRecorderLimit shapes.
func NewRecorderWithLimit ¶
NewRecorderWithLimit returns a recorder holding at most limit distinct shapes. Once full it stops adding new ones and keeps counting the ones it has, so memory is bounded regardless of what arrives.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds custom rules layered over the built-in database.
Registering rebuilds the compiled indexes and swaps them in atomically, so a registry can be updated while requests are in flight: readers never block and never observe a half-built database.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry returns an empty registry that resolves exactly like the built-in database until rules are added.
func (*Registry) RegisterApp ¶
RegisterApp adds in-app WebView rules.
func (*Registry) RegisterAutomation ¶
RegisterAutomation adds automation and headless rules.
func (*Registry) RegisterBot ¶
RegisterBot adds bot rules.
func (*Registry) RegisterBrowser ¶
RegisterBrowser adds browser rules.
func (*Registry) RegisterEngine ¶
RegisterEngine adds layout engine rules.
func (*Registry) RegisterHTTPClient ¶
RegisterHTTPClient adds HTTP client and CLI tool rules.
func (*Registry) RegisterOS ¶
RegisterOS adds operating-system rules.
type Rule ¶
type Rule struct {
ID string
Name string
Family string
Engine string
Brand string
Model string
Series string
Category string
Provider string
Type string
Year int
Token string
Prefix string
Contains string
// VersionFrom selects a version-extraction strategy: "token",
// "version_product", "rv", "apple", "nt", "cros" or "suffix". Empty means
// the rule carries no version.
VersionFrom string
// Priority decides which rule wins when several match. Built-in rules use
// 50–320; a custom rule at 1000 always wins.
Priority int
// Confidence must be in (0,1]. It is reported verbatim on the resolved
// field, so it should reflect how much the match really proves.
Confidence float32
}
Rule is a caller-supplied classification rule.
It exists so a deployment can teach uax about clients no public database will ever carry — an internal mobile app, a partner's crawler, a company HTTP client. Custom rules are merged with the built-in database and compete on Priority alone, so a rule can also override a built-in one by outranking it.
Exactly one match form must be set:
Token exact match against a product token or a comment field Prefix the token or field starts with this string Contains substring anywhere in the input — correct, but the slowest form
type Source ¶
type Source string
Source records where a resolved field came from. Client Hints are authoritative; a value inferred from a sibling field is not.
type Verdict ¶
type Verdict struct {
Kind Kind `json:"kind"`
Trust float32 `json:"trust"`
Risk float32 `json:"risk"`
Reasons []Reason `json:"reasons,omitempty"`
Recommend Action `json:"recommend"`
}
Verdict is the actionable summary of everything uax determined.
The two scores answer different questions and should not be conflated:
Trust is how much the client's self-description can be believed. A verified browser sending coherent hints scores high. An honest, well-behaved crawler also scores high — being a bot is not being a liar. Risk is how likely this request is to be hostile. A client whose claims contradict each other scores high, whatever it claims to be.
func ComputeVerdict ¶
ComputeVerdict derives a verdict from an already-parsed client.
func VerdictFrom ¶
func VerdictFrom(c *Client, cons Consistency) *Verdict
VerdictFrom derives a verdict from a client and a coherence check already performed on it.
A verdict is built on top of the consistency findings, so ComputeVerdict has to run the spoof rules to produce one. Callers that display the findings as well — the CLI, the MCP server, ParseRequest — would otherwise run them twice per request. Pass a Consistency from any source but this client and the verdict describes a request that never arrived.
type Version ¶
type Version struct {
Major int `json:"major,omitempty"`
Minor int `json:"minor,omitempty"`
Patch int `json:"patch,omitempty"`
KnownVersion bool `json:"known_version,omitempty"`
Raw string `json:"raw,omitempty"`
}
Version represents a parsed version string.
func ParseVersion ¶
ParseVersion parses a version string (e.g., "1.2.3") without allocating memory for parts.
func (Version) AtLeast ¶
AtLeast checks if the version is greater than or equal to the specified parts.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
uax
command
Command uax inspects User-Agent strings from the command line.
|
Command uax inspects User-Agent strings from the command line. |
|
uax-mcp
command
Command uax-mcp exposes uax over the Model Context Protocol.
|
Command uax-mcp exposes uax over the Model Context Protocol. |
|
internal
|
|
|
table
Package table is the classification database.
|
Package table is the classification database. |
|
Package middleware attaches a parsed uax.Client to the request context.
|
Package middleware attaches a parsed uax.Client to the request context. |
|
echo
module
|
|
|
fiber
module
|
|
|
gin
module
|
|
|
Package robots renders a robots.txt from what uax knows about crawlers.
|
Package robots renders a robots.txt from what uax knows about crawlers. |
|
Package uatest builds realistic User-Agent strings and requests for tests.
|
Package uatest builds realistic User-Agent strings and requests for tests. |