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
- Variables
- func Key(enabled bool, signingKey string) []byte
- func Refusal(err error) bool
- func ValidateName(name string) error
- type AnswerStore
- type Challenge
- type Check
- type Checker
- type InUseError
- type Input
- type PostgresAnswers
- type PostgresChecks
- func (p *PostgresChecks) Delete(ctx context.Context, name string) error
- func (p *PostgresChecks) Get(ctx context.Context, name string) (Check, error)
- func (p *PostgresChecks) List(ctx context.Context) ([]Check, error)
- func (p *PostgresChecks) Put(ctx context.Context, c Check, author string) (Check, error)
- func (p *PostgresChecks) UsedBy(ctx context.Context) (map[string][]string, error)
- type Store
- type Verifier
Constants ¶
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.
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 ( TurnstileVerifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify" HCaptchaVerifyURL = "https://api.hcaptcha.com/siteverify" )
The providers' server-side verification endpoints.
const DefaultName = "default"
DefaultName is the check migration 000221 made from the deployment's one bot check setting.
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.
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") ErrOtherCheck = errors.New("the bot check challenge was issued by another bot check") )
Refusals a check answers with. Each is the reason a request was refused, and none says more about the deployment than that.
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.
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.
func Refusal ¶
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
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 (Check) NeedsBrowser ¶ added in v1.152.0
NeedsBrowser reports whether answering the check takes a browser, which the proof of work does not.
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 ¶
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.
type InUseError ¶ added in v1.152.0
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.
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 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) List ¶ added in v1.152.0
func (p *PostgresChecks) List(ctx context.Context) ([]Check, error)
List reads every check, by name.
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.