Skip to main content

Overview

The challenge package defines the challenge system for bot detection. Challenges are cryptographic or computational puzzles that legitimate browsers can solve but bots typically cannot.

Types

Challenge

Metadata about a single challenge issuance.
type Challenge struct {
	IssuedAt       time.Time         // When the challenge was issued
	Metadata       map[string]string // Challenge metadata (IP, User-Agent)
	ID             string            // UUID identifying the challenge
	Method         string            // Challenge method ("fast", "preact", etc.)
	RandomData     string            // Hex-encoded random data to process
	PolicyRuleHash string            // Hash of the policy rule that issued this
	Difficulty     int               // Difficulty level (0-64)
	Spent          bool              // Has challenge been solved?
}
IssuedAt
time.Time
Timestamp when the challenge was created
Metadata
map[string]string
Additional context stored with the challengeCommon keys:
  • User-Agent: Client user agent string
  • X-Real-Ip: Client IP address
ID
string
Unique UUID (v7) identifier for this challenge
Method
string
Challenge algorithm nameBuilt-in methods:
  • fast: Proof-of-work (SHA-256 based)
  • preact: Interactive JavaScript challenge
  • metarefresh: Meta refresh redirect challenge
RandomData
string
Hexadecimal-encoded random bytes (64 bytes) that the client must process
PolicyRuleHash
string
Hash of the bot policy rule that triggered this challenge. Used to detect policy changes.
Difficulty
int
Computational difficulty for proof-of-work challenges (0-64). Higher values require more CPU time.Recommended values:
  • 15-18: Low security, fast solving (~100ms)
  • 19-22: Medium security (~500ms)
  • 23-25: High security (~2-5s)
  • 26+: Very high security (10s+)
Spent
bool
Whether this challenge has already been successfully solved. Prevents replay attacks.
Example
import (
	"time"
	"github.com/google/uuid"
	"github.com/TecharoHQ/anubis/lib/challenge"
)

// Create a challenge
chall := &challenge.Challenge{
	ID:         uuid.NewString(),
	Method:     "fast",
	RandomData: "a1b2c3d4...", // hex encoded
	IssuedAt:   time.Now(),
	Difficulty: 20,
	Metadata: map[string]string{
		"User-Agent": r.Header.Get("User-Agent"),
		"X-Real-Ip":  r.Header.Get("X-Real-Ip"),
	},
}
See lib/challenge/challenge.go:5-15

IssueInput

Input parameters for the challenge Issue method.
type IssueInput struct {
	Impressum *config.Impressum
	Rule      *policy.Bot
	Challenge *Challenge
	OGTags    map[string]string
	Store     store.Interface
}
Impressum
*config.Impressum
Legal/contact information to display on challenge page
Rule
*policy.Bot
The bot detection rule that triggered this challenge
Challenge
*Challenge
The challenge instance being issued
OGTags
map[string]string
OpenGraph metadata tags for the page being protected
Store
store.Interface
Storage backend for persisting challenge state
See lib/challenge/interface.go:45-51

ValidateInput

Input parameters for the challenge Validate method.
type ValidateInput struct {
	Rule      *policy.Bot
	Challenge *Challenge
	Store     store.Interface
}
Rule
*policy.Bot
The policy rule associated with this challenge
Challenge
*Challenge
The challenge being validated
Store
store.Interface
Storage backend for challenge data
See lib/challenge/interface.go:53-57

Interfaces

Impl

Interface for challenge algorithm implementations.
type Impl interface {
	// Setup registers HTTP routes for challenge assets/APIs
	Setup(mux *http.ServeMux)

	// Issue creates a challenge page component
	Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *IssueInput) (templ.Component, error)

	// Validate checks if a challenge solution is correct
	Validate(r *http.Request, lg *slog.Logger, in *ValidateInput) error
}

Setup

Registers any HTTP routes needed by the challenge implementation (e.g., for serving JavaScript bundles or API endpoints).
Setup(mux *http.ServeMux)
mux
*http.ServeMux
HTTP router to register routes with
Example
func (impl *MyChallenge) Setup(mux *http.ServeMux) {
	mux.Handle("/api/challenge/assets/script.js", 
		http.HandlerFunc(impl.serveScript))
}

Issue

Generates the challenge page component to display to the user.
Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *IssueInput) (templ.Component, error)
w
http.ResponseWriter
HTTP response writer (for setting headers)
r
*http.Request
HTTP request being challenged
lg
*slog.Logger
Structured logger with request context
in
*IssueInput
Challenge issuance parameters
component
templ.Component
Templ component to render as the challenge page
error
error
Error if challenge generation fails
Example
func (impl *PoW) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *IssueInput) (templ.Component, error) {
	return web.ProofOfWorkPage(in.Challenge, in.Rule), nil
}
See lib/challenge/interface.go:64

Validate

Validates that the user correctly solved the challenge.
Validate(r *http.Request, lg *slog.Logger, in *ValidateInput) error
r
*http.Request
HTTP request containing the challenge solution
lg
*slog.Logger
Structured logger with request context
in
*ValidateInput
Challenge validation parameters
error
error
Returns nil if validation succeeds, or an error describing why validation failed
Validation Errors: Return a *challenge.Error for user-facing validation failures:
if solution != expected {
	return challenge.NewError("validate", "Incorrect solution", challenge.ErrFailed)
}
See lib/challenge/interface.go:67

Functions

Register

Registers a challenge implementation with the global registry.
func Register(name string, impl Impl)
name
string
required
Unique name for the challenge algorithm (e.g., “fast”, “preact”)
impl
Impl
required
Challenge implementation
Example
func init() {
	challenge.Register("mychal", &MyChallenge{)
}
See lib/challenge/interface.go:20-24

Get

Retrieves a registered challenge implementation by name.
func Get(name string) (Impl, bool)
name
string
required
Challenge algorithm name
impl
Impl
The challenge implementation, if found
ok
bool
True if the challenge exists in the registry
Example
impl, ok := challenge.Get("fast")
if !ok {
	log.Fatal("challenge algorithm not found")
}
See lib/challenge/interface.go:27-32

Methods

Returns a sorted list of all registered challenge algorithm names.
func Methods() []string
methods
[]string
Sorted slice of challenge algorithm names
Example
for _, method := range challenge.Methods() {
	fmt.Println("Available challenge:", method)
}
// Output:
// Available challenge: fast
// Available challenge: metarefresh
// Available challenge: preact
See lib/challenge/interface.go:34-43

Error Types

Error

Challenge validation error with public and private messages.
type Error struct {
	PrivateReason error  // Internal error (logged)
	Verb          string // Action being performed
	PublicReason  string // User-facing error message
	StatusCode    int    // HTTP status code
}
PrivateReason
error
Internal error details (not shown to users)
Verb
string
Action that failed (e.g., “validate”, “decode”)
PublicReason
string
User-friendly error message displayed on error page
StatusCode
int
HTTP status code for the error response (default: 403)
See lib/challenge/error.go:24-29

NewError

Creates a new challenge error.
func NewError(verb, publicReason string, privateReason error) *Error
verb
string
required
Action being performed when error occurred
publicReason
string
required
User-facing error description
privateReason
error
required
Internal error to log and wrap
error
*Error
Challenge error with status code 403
Example
if nonce == "" {
	return challenge.NewError(
		"validate",
		"Missing required field: nonce",
		challenge.ErrMissingField,
	)
}
See lib/challenge/error.go:15-21

Sentinel Errors

var (
	ErrFailed        = errors.New("challenge: user failed challenge")
	ErrMissingField  = errors.New("challenge: missing field")
	ErrInvalidFormat = errors.New("challenge: field has invalid format")
)
ErrFailed
error
User submitted an incorrect solution
ErrMissingField
error
Required field missing from request
ErrInvalidFormat
error
Field has incorrect format or encoding
See lib/challenge/error.go:9-13

Built-in Challenges

Anubis includes three challenge implementations:

fast (Proof of Work)

SHA-256 based proof-of-work challenge. Client must find a nonce that produces a hash with N leading zero bits. Difficulty mapping: Each difficulty level adds one zero bit requirement.

preact (Interactive)

React-based interactive challenge requiring user interaction. Tests JavaScript execution and user behavior.

metarefresh

Meta refresh redirect challenge. Tests basic HTML parsing and redirect following.

Build docs developers (and LLMs) love