botcheck

package
v1.148.0 Latest Latest
Warning

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

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

Documentation

Overview

Package botcheck is the bot check a public endpoint requires (#2173): by default a proof-of-work challenge the platform issues and verifies itself, or Cloudflare Turnstile or hCaptcha verified server side. A challenge is signed, short-lived and bound to 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 challenge asks for rises with the endpoint's recent request rate. The configuration is the platform_settings section "bot_check", which administrators edit under Admin > Settings > Bot check.

Index

Constants

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 (
	// ProviderPoW is the platform's own proof-of-work challenge, the default:
	// it calls no third party.
	ProviderPoW       = "pow"
	ProviderTurnstile = "turnstile"
	ProviderHCaptcha  = "hcaptcha"
	// ProviderNone checks nothing. A public endpoint can then be bound only
	// when an administrator allows it (AllowPublicWithoutCheck).
	ProviderNone = "none"
)

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 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.

View Source
const SettingsSection = "bot_check"

SettingsSection is the platform_settings section holding the settings.

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")
	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 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).

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 {
	// Provider is pow, turnstile, hcaptcha or none.
	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 public endpoint.

type Checker

type Checker struct {
	Settings SettingsStore
	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 bot checks under the deployment's settings.

func New

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

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

func (*Checker) Current

func (c *Checker) Current(ctx context.Context) (Settings, error)

Current is the deployment's settings as they stand.

func (*Checker) Issue

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

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

func (*Checker) PublicAllowed

func (c *Checker) PublicAllowed(ctx context.Context) (bool, error)

PublicAllowed reports whether a public endpoint may be bound and answered (Settings.PublicAllowed). Proof of work counts only where there is a key to sign challenges with.

func (*Checker) Verify

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

Verify accepts answer for endpoint, or returns why it is refused, and records an accepted answer as used. With the provider none it accepts everything; whether a public endpoint may run without a check at all is the binding's question (PublicAllowed), asked before this.

type Input

type Input struct {
	Provider                *string `json:"provider,omitempty"`
	SiteKey                 *string `json:"site_key,omitempty"`
	SecretName              *string `json:"secret_name,omitempty"`
	AllowPublicWithoutCheck *bool   `json:"allow_public_without_check,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.

func (Input) Apply

func (in Input) Apply(stored Settings) (Settings, 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 Settings

type Settings struct {
	// Provider is pow, turnstile, hcaptcha or none.
	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"`
	// AllowPublicWithoutCheck lets a public endpoint be bound and answered
	// while the provider is none.
	AllowPublicWithoutCheck bool `json:"allow_public_without_check"`
	// 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"`
}

Settings is the stored configuration.

func Defaults

func Defaults() Settings

Defaults is the configuration before an administrator writes the section: the proof-of-work challenge, which needs no setup.

func (Settings) Checks

func (s Settings) Checks() bool

Checks reports whether the provider checks anything.

func (Settings) PublicAllowed

func (s Settings) PublicAllowed() bool

PublicAllowed reports whether a public endpoint may be bound and answered: there is a bot check, or an administrator allowed public endpoints without one.

func (Settings) PublicAllowedWhere

func (s Settings) PublicAllowedWhere(canSign bool) bool

PublicAllowedWhere is PublicAllowed on a deployment that can, or cannot, sign a proof-of-work challenge: one that cannot has no proof of work.

func (Settings) Validate

func (s Settings) Validate() error

Validate reports the first thing wrong with the settings.

type SettingsStore

type SettingsStore interface {
	Get(ctx context.Context) (settingsjson.Stored[Settings], error)
	Set(ctx context.Context, v Settings, author string) error
}

SettingsStore reads and writes the section.

func NewSettingsStore

func NewSettingsStore(db *sql.DB) SettingsStore

NewSettingsStore is the section over db.

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.

type View

type View struct {
	Settings
	UpdatedBy string `json:"updated_by,omitempty"`
	UpdatedAt string `json:"updated_at,omitempty"`
}

View is the settings as an administrator reads them.

func Read

func Read(ctx context.Context, store SettingsStore) (View, error)

Read returns the stored settings, or the defaults when none were written.

Jump to

Keyboard shortcuts

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