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 ¶
const ( TurnstileVerifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify" HCaptchaVerifyURL = "https://api.hcaptcha.com/siteverify" )
The providers' server-side verification endpoints.
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.
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.
const KeyLabel = "bot-check-challenge-v1"
KeyLabel domain-separates the challenge signing key from the master key it is derived from.
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.
const SettingsSection = "bot_check"
SettingsSection is the platform_settings section holding the settings.
Variables ¶
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") )
Refusals a check answers with. Each is the reason a request was refused, and none says more about the deployment than that.
var SolverJS []byte
SolverJS is the browser script a page answers a bot check with, served at /botcheck/solver.js.
Functions ¶
func Key ¶
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.
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) Issue ¶
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 ¶
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.
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.
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 ¶
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) PublicAllowed ¶
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 ¶
PublicAllowedWhere is PublicAllowed on a deployment that can, or cannot, sign a proof-of-work challenge: one that cannot has no proof of work.
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.