Documentation Index
Fetch the complete documentation index at: https://mintlify.com/TecharoHq/Anubis/llms.txt
Use this file to discover all available pages before exploring further.
Anubis supports custom challenge types through the challenge.Impl interface. This allows you to create specialized bot detection mechanisms beyond the built-in proof-of-work, meta-refresh, and Preact challenges.
Challenge Interface
All challenge implementations must satisfy the challenge.Impl interface:
type Impl interface {
// Setup registers any additional routes with the Impl for assets or API routes.
Setup(mux *http.ServeMux)
// Issue a new challenge to the user, called by the Anubis.
Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *IssueInput) (templ.Component, error)
// Validate a challenge, making sure that it passes muster.
Validate(r *http.Request, lg *slog.Logger, in *ValidateInput) error
}
Source: lib/challenge/interface.go:59-68
Provided when issuing a new challenge:
type IssueInput struct {
Impressum *config.Impressum
Rule *policy.Bot
Challenge *Challenge
OGTags map[string]string
Store store.Interface
}
Provided when validating a challenge response:
type ValidateInput struct {
Rule *policy.Bot
Challenge *Challenge
Store store.Interface
}
type Challenge struct {
IssuedAt time.Time `json:"issuedAt"`
Metadata map[string]string `json:"metadata"`
ID string `json:"id"`
Method string `json:"method"`
RandomData string `json:"randomData"`
PolicyRuleHash string `json:"policyRuleHash,omitempty"`
Difficulty int `json:"difficulty,omitempty"`
Spent bool `json:"spent"`
}
Source: lib/challenge/challenge.go:6-15
Implementation Example: Proof-of-Work
Here’s how the built-in proof-of-work challenge is implemented:
package proofofwork
import (
"crypto/subtle"
"fmt"
"log/slog"
"net/http"
"strconv"
"strings"
chall "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/localization"
"github.com/a-h/templ"
)
func init() {
chall.Register("fast", &Impl{Algorithm: "fast")
chall.Register("slow", &Impl{Algorithm: "slow")
}
type Impl struct {
Algorithm string
}
func (i *Impl) Setup(mux *http.ServeMux) {}
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
loc := localization.GetLocalizer(r)
return page(loc), nil
}
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
rule := in.Rule
challenge := in.Challenge.RandomData
nonceStr := r.FormValue("nonce")
if nonceStr == "" {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField))
}
nonce, err := strconv.Atoi(nonceStr)
if err != nil {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: nonce: %w", chall.ErrInvalidFormat, err))
}
response := r.FormValue("response")
if response == "" {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField))
}
calcString := fmt.Sprintf("%s%d", challenge, nonce)
calculated := internal.SHA256sum(calcString)
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", chall.ErrFailed, calculated, response))
}
// compare the leading zeroes
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted %d leading zeros but got %s", chall.ErrFailed, rule.Challenge.Difficulty, response))
}
return nil
}
Source: lib/challenge/proofofwork/proofofwork.go
The meta-refresh challenge validates timing constraints:
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 800 * time.Millisecond)
if time.Now().Before(wantTime) {
return challenge.NewError("validate", "insufficient time",
fmt.Errorf("%w: wanted user to wait until at least %s", challenge.ErrFailed, wantTime.Format(time.RFC3339)))
}
gotChallenge := r.FormValue("challenge")
if subtle.ConstantTimeCompare([]byte(in.Challenge.RandomData), []byte(gotChallenge)) != 1 {
return challenge.NewError("validate", "invalid response",
fmt.Errorf("%w: wanted response %s but got %s", challenge.ErrFailed, in.Challenge.RandomData, gotChallenge))
}
return nil
}
Source: lib/challenge/metarefresh/metarefresh.go:51-64
Registration
Register your challenge implementation in an init() function:
func init() {
challenge.Register("mycustom", &MyCustomImpl{)
}
The registry is thread-safe and uses sync.RWMutex for concurrent access.
Source: lib/challenge/interface.go:15-32
Error Handling
Use the challenge error constructors for consistent error reporting:
var (
ErrFailed = errors.New("challenge: user failed challenge")
ErrMissingField = errors.New("challenge: missing field")
ErrInvalidFormat = errors.New("challenge: field has invalid format")
)
func NewError(verb, publicReason string, privateReason error) *Error {
return &Error{
Verb: verb,
PublicReason: publicReason,
PrivateReason: privateReason,
StatusCode: http.StatusForbidden,
}
}
Source: lib/challenge/error.go:9-22
Best Practices
- Security-first: Use constant-time comparison for secrets (
crypto/subtle.ConstantTimeCompare)
- Difficulty scaling: Honor
in.Rule.Challenge.Difficulty from the policy configuration
- Localization: Use
localization.GetLocalizer(r) for internationalized UI
- Metrics: Emit Prometheus metrics for observability (see
lib/challenge/metrics.go)
- Structured logging: Use the provided
*slog.Logger for diagnostic output
- Templ components: Return
templ.Component for HTML rendering consistency
Configuration
Once registered, reference your challenge in policy files:
bots:
- name: custom-challenge-rule
action: challenge
expression:
- path.startsWith('/protected')
challenge:
algorithm: mycustom
difficulty: 5
Available Challenge Methods
Query registered challenges at runtime:
methods := challenge.Methods() // Returns []string of all registered challenge types
impl, ok := challenge.Get("fast") // Get a specific implementation
Source: lib/challenge/interface.go:27-42