Help us shape the future of Go! Take the 2026 Go Developer Survey

botcheck

package
v1.152.0 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Overview

Package botcheck is the bot check a bot_check endpoint requires (#2173, #2259): a named check an administrator configures and an endpoint picks, either the platform's own proof-of-work challenge or Cloudflare Turnstile or hCaptcha verified server side. A challenge is signed, short-lived and bound to one check and one endpoint, and an answer is recorded as used in PostgreSQL the moment it is accepted, so it cannot be replayed on any replica. The work a proof-of-work challenge asks for rises with the endpoint's recent request rate. The checks are the bot_checks table, which administrators edit under Admin > Settings > Bot checks.

Index

Constants

View Source
const (
	// ProviderPoW is the platform's own proof-of-work challenge: it calls no
	// third party. It makes each request cost work, which any program can
	// pay; it slows an automated caller rather than stopping one.
	ProviderPoW = "pow"
	// ProviderTurnstile and ProviderHCaptcha need a browser to answer.
	ProviderTurnstile = "turnstile"
	ProviderHCaptcha  = "hcaptcha"
)

Providers.

View Source
const (
	DefaultDifficulty    = 16
	DefaultMaxDifficulty = 22
	DefaultRateThreshold = 30
	DefaultChallengeTTL  = 300
)

Defaults and bounds of the proof-of-work challenge. Difficulty is the number of leading zero bits the answer's SHA-256 must have: 16 is about 65,000 hashes, a fraction of a second in a browser.

View Source
const (
	TurnstileVerifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
	HCaptchaVerifyURL  = "https://api.hcaptcha.com/siteverify"
)

The providers' server-side verification endpoints.

View Source
const DefaultName = "default"

DefaultName is the check migration 000221 made from the deployment's one bot check setting.

View Source
const KeyLabel = "bot-check-challenge-v1"

KeyLabel domain-separates the challenge signing key from the master key it is derived from.

View Source
const SecretUse = "bot_check"

SecretUse is what a stored secret's allow_connections names for the bot check to read it: the provider's secret key goes to the provider through the platform, never through a connection a call names.

Variables

View Source
var (
	ErrMissing       = errors.New("the request carries no bot check answer")
	ErrInvalid       = errors.New("the bot check answer is not valid")
	ErrExpired       = errors.New("the bot check challenge has expired")
	ErrReplayed      = errors.New("the bot check answer was already used")
	ErrOtherEndpoint = errors.New("the bot check challenge was issued for another endpoint")
	ErrOtherCheck    = errors.New("the bot check challenge was issued by another bot check")
	ErrUnavailable   = errors.New("this deployment cannot issue a bot check challenge")
)

Refusals a check answers with. Each is the reason a request was refused, and none says more about the deployment than that.

View Source
var (
	// ErrNotFound reports a name no check has.
	ErrNotFound = errors.New("no bot check has this name")
)

Errors a check is looked up, written or deleted with.

View Source
var SolverJS []byte

SolverJS is the browser script a page answers a bot check with, served at /botcheck/solver.js.

Functions

func Key

func Key(enabled bool, signingKey string) []byte

Key is the challenge signing key, derived from the browser-session signing key (base64, auth.browser_session.signing_key) that every replica holds, so a challenge one replica issues verifies on another. Nil when browser sessions are off or the key does not decode: such a deployment cannot issue a proof-of-work challenge.

func Refusal

func Refusal(err error) bool

Refusal reports whether err is a refusal of the answer, which is the caller's (403), rather than the check being unable to run (503).

func ValidateName added in v1.152.0

func ValidateName(name string) error

ValidateName refuses a name a check cannot have.

Types

type AnswerStore

type AnswerStore interface {
	// Use records challengeID as answered for endpoint, kept until expires,
	// and reports false when it was answered before.
	Use(ctx context.Context, challengeID, endpoint string, expires time.Time) (bool, error)
	// Recent counts the challenges endpoint had answered since since.
	Recent(ctx context.Context, endpoint string, since time.Time) (int, error)
	// Sweep deletes the answers whose challenge expired before now.
	Sweep(ctx context.Context, now time.Time) (int64, error)
}

AnswerStore records the challenges answered, which is what refuses a replay on every replica and what an endpoint's recent rate is counted from.

type Challenge

type Challenge struct {
	// Check is the name of the check the endpoint names.
	Check string `json:"check" example:"default"`
	// Provider is pow, turnstile or hcaptcha.
	Provider string `json:"provider" example:"pow"`
	// Token is the proof-of-work challenge; the answer is Token~counter, the
	// counter whose SHA-256 of Token~counter has Difficulty leading zero bits.
	Token      string `json:"token,omitempty"`
	Difficulty int    `json:"difficulty,omitempty"`
	// SiteKey is the Turnstile or hCaptcha widget's site key; the answer is
	// the token the widget produces.
	SiteKey   string    `json:"site_key,omitempty"`
	ExpiresAt time.Time `json:"expires_at,omitzero"`
}

Challenge is what a page is handed to answer before it calls a bot_check endpoint.

type Check added in v1.152.0

type Check struct {
	// Name is how an endpoint names the check.
	Name string `json:"name" example:"default"`
	// Provider is pow, turnstile or hcaptcha.
	Provider string `json:"provider" example:"pow"`
	// SiteKey is the Turnstile or hCaptcha site key a page renders the
	// widget with. It is public.
	SiteKey string `json:"site_key"`
	// SecretName is the stored secret (Admin > Secrets) holding the
	// provider's secret key, whose allow_connections names SecretUse and
	// which lists no personas. The key never leaves the server.
	SecretName string `json:"secret_name"`
	// Difficulty is the proof-of-work's leading zero bits while an endpoint
	// is quiet, and MaxDifficulty the most a flood raises it to.
	Difficulty    int `json:"difficulty"`
	MaxDifficulty int `json:"max_difficulty"`
	// RateThreshold is the answers a minute an endpoint takes before each
	// doubling of its rate adds a bit of difficulty.
	RateThreshold int `json:"rate_threshold"`
	// ChallengeTTLSeconds is how long a challenge may be answered.
	ChallengeTTLSeconds int       `json:"challenge_ttl_seconds"`
	CreatedBy           string    `json:"created_by,omitempty"`
	UpdatedBy           string    `json:"updated_by,omitempty"`
	CreatedAt           time.Time `json:"created_at,omitzero"`
	UpdatedAt           time.Time `json:"updated_at,omitzero"`
}

Check is one named bot check.

func New

func New(name string) Check

New is a check named name at the proof of work's defaults.

func (Check) NeedsBrowser added in v1.152.0

func (c Check) NeedsBrowser() bool

NeedsBrowser reports whether answering the check takes a browser, which the proof of work does not.

func (Check) Validate added in v1.152.0

func (c Check) Validate() error

Validate reports the first thing wrong with the check.

type Checker

type Checker struct {
	Checks  Store
	Answers AnswerStore
	// Key signs proof-of-work challenges; nil leaves the deployment unable to
	// issue one.
	Key []byte
	// Secret reads the stored secret holding a provider's secret key.
	Secret func(ctx context.Context, name string) (string, error)
	// Verifier asks a provider about a token.
	Verifier interface {
		Verify(ctx context.Context, provider, secret, token string) error
	}
	// Now is the clock; nil is time.Now.
	Now func() time.Time
}

Checker issues and verifies the named checks.

func NewChecker added in v1.152.0

func NewChecker(db *sql.DB, key []byte, secret func(ctx context.Context, name string) (string, error)) *Checker

NewChecker is the checker over db: its checks and its answer table. key signs proof-of-work challenges (Key); secret reads a provider's key.

func (*Checker) Issue

func (c *Checker) Issue(ctx context.Context, check, endpoint string) (Challenge, error)

Issue is the challenge a page answers before it calls endpoint, under the check named check. A proof of work asks for more as the endpoint's recent rate rises.

func (*Checker) Usable added in v1.152.0

func (c *Checker) Usable(ctx context.Context, name string) error

Usable reports why an endpoint cannot be bound to the check named name: no check has the name, or it is a proof of work and the deployment has no key to sign one with. Nil when it can.

func (*Checker) Verify

func (c *Checker) Verify(ctx context.Context, check, endpoint, answer string) error

Verify accepts answer for endpoint under the check named check, or returns why it is refused, and records an accepted answer as used.

type InUseError added in v1.152.0

type InUseError struct {
	Name      string
	Endpoints []string
}

InUseError refuses the deletion of a check endpoints name.

func (*InUseError) Error added in v1.152.0

func (e *InUseError) Error() string

type Input

type Input struct {
	Provider            *string `json:"provider,omitempty"`
	SiteKey             *string `json:"site_key,omitempty"`
	SecretName          *string `json:"secret_name,omitempty"`
	Difficulty          *int    `json:"difficulty,omitempty"`
	MaxDifficulty       *int    `json:"max_difficulty,omitempty"`
	RateThreshold       *int    `json:"rate_threshold,omitempty"`
	ChallengeTTLSeconds *int    `json:"challenge_ttl_seconds,omitempty"`
}

Input is the write shape. An absent field keeps the stored value, or the default on a new check.

func (Input) Apply

func (in Input) Apply(stored Check) (Check, error)

Apply returns stored with in written over it, or the first reason the result is invalid. Whether the named secret exists is the route's to check.

type PostgresAnswers

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

PostgresAnswers is the AnswerStore over the bot_check_answers table.

func NewAnswerStore

func NewAnswerStore(db *sql.DB) *PostgresAnswers

NewAnswerStore is the answer store over db.

func (*PostgresAnswers) Recent

func (p *PostgresAnswers) Recent(ctx context.Context, endpoint string, since time.Time) (int, error)

Recent counts endpoint's answers since since.

func (*PostgresAnswers) Sweep

func (p *PostgresAnswers) Sweep(ctx context.Context, now time.Time) (int64, error)

Sweep deletes the answers whose challenge can no longer be answered.

func (*PostgresAnswers) Use

func (p *PostgresAnswers) Use(ctx context.Context, challengeID, endpoint string, expires time.Time) (bool, error)

Use inserts the answer, and the primary key decides whether it was the first: two replicas handed the same answer at once both insert, and one insert finds the row the other wrote.

type PostgresChecks added in v1.152.0

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

PostgresChecks is the Store over the bot_checks table of migration 000221.

func NewStore added in v1.152.0

func NewStore(db *sql.DB) *PostgresChecks

NewStore is the check store over db.

func (*PostgresChecks) Delete added in v1.152.0

func (p *PostgresChecks) Delete(ctx context.Context, name string) error

Delete removes the check. The foreign key refuses one an endpoint names, on every replica, and the refusal names those endpoints.

func (*PostgresChecks) Get added in v1.152.0

func (p *PostgresChecks) Get(ctx context.Context, name string) (Check, error)

Get reads one check.

func (*PostgresChecks) List added in v1.152.0

func (p *PostgresChecks) List(ctx context.Context) ([]Check, error)

List reads every check, by name.

func (*PostgresChecks) Put added in v1.152.0

func (p *PostgresChecks) Put(ctx context.Context, c Check, author string) (Check, error)

Put upserts the check by name.

func (*PostgresChecks) UsedBy added in v1.152.0

func (p *PostgresChecks) UsedBy(ctx context.Context) (map[string][]string, error)

UsedBy reads which endpoints name each check.

type Store added in v1.152.0

type Store interface {
	// List is every check, by name.
	List(ctx context.Context) ([]Check, error)
	// Get is one check, ErrNotFound when no check has the name.
	Get(ctx context.Context, name string) (Check, error)
	// Put creates or replaces a check, keeping the creation stamp of the one
	// it replaces.
	Put(ctx context.Context, c Check, author string) (Check, error)
	// Delete removes a check, ErrNotFound when none has the name and an
	// *InUseError naming the endpoints when any names it.
	Delete(ctx context.Context, name string) error
	// UsedBy is each check's endpoints, by check name.
	UsedBy(ctx context.Context) (map[string][]string, error)
}

Store keeps the checks.

type Verifier

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

Verifier asks a provider whether a token its widget produced is genuine.

func NewVerifier

func NewVerifier() *Verifier

NewVerifier is a Verifier over the platform's outbound client.

func (*Verifier) Verify

func (v *Verifier) Verify(ctx context.Context, provider, secret, token string) error

Verify posts token and secret to provider's verification endpoint, and returns nil only when the provider says the token is genuine. A provider that did not answer is an error distinct from ErrInvalid: the request was not shown to be a bot, the check could not be made.

Jump to

Keyboard shortcuts

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