mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-12 03:28:45 +00:00
feat(lib): use new challenge creation flow
Previously Anubis constructed challenge strings from request metadata. This was a good idea in spirit, but has turned out to be a very bad idea in practice. This new flow reuses the Store facility to dynamically create challenge values with completely random data. This is a fairly big rewrite of how Anubis processes challenges. Right now it defaults to using the in-memory storage backend, but on-disk (boltdb) and valkey-based adaptors will come soon. Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
@@ -1,65 +1,11 @@
|
||||
package challenge
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
import "time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
var (
|
||||
registry map[string]Impl = map[string]Impl{}
|
||||
regLock sync.RWMutex
|
||||
)
|
||||
|
||||
func Register(name string, impl Impl) {
|
||||
regLock.Lock()
|
||||
defer regLock.Unlock()
|
||||
|
||||
registry[name] = impl
|
||||
}
|
||||
|
||||
func Get(name string) (Impl, bool) {
|
||||
regLock.RLock()
|
||||
defer regLock.RUnlock()
|
||||
result, ok := registry[name]
|
||||
return result, ok
|
||||
}
|
||||
|
||||
func Methods() []string {
|
||||
regLock.RLock()
|
||||
defer regLock.RUnlock()
|
||||
var result []string
|
||||
for method := range registry {
|
||||
result = append(result, method)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
type IssueInput struct {
|
||||
Impressum *config.Impressum
|
||||
Rule *policy.Bot
|
||||
Challenge string
|
||||
OGTags map[string]string
|
||||
}
|
||||
|
||||
type ValidateInput struct {
|
||||
Rule *policy.Bot
|
||||
Challenge string
|
||||
}
|
||||
|
||||
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(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
|
||||
// Challenge is the metadata about a single challenge issuance.
|
||||
type Challenge struct {
|
||||
ID string `json:"id"` // UUID identifying the challenge
|
||||
RandomData string `json:"randomData"` // The random data the client processes
|
||||
IssuedAt time.Time `json:"issuedAt"` // When the challenge was issued
|
||||
Metadata map[string]string `json:"metadata"` // Challenge metadata such as IP address and user agent
|
||||
}
|
||||
|
||||
23
lib/challenge/challengetest/challengetest.go
Normal file
23
lib/challenge/challengetest/challengetest.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package challengetest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func New(t *testing.T) *challenge.Challenge {
|
||||
t.Helper()
|
||||
|
||||
id := uuid.Must(uuid.NewV7())
|
||||
randomData := internal.SHA256sum(time.Now().String())
|
||||
|
||||
return &challenge.Challenge{
|
||||
ID: id.String(),
|
||||
RandomData: randomData,
|
||||
IssuedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
7
lib/challenge/challengetest/challengetest_test.go
Normal file
7
lib/challenge/challengetest/challengetest_test.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package challengetest
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
_ = New(t)
|
||||
}
|
||||
68
lib/challenge/interface.go
Normal file
68
lib/challenge/interface.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package challenge
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
var (
|
||||
registry map[string]Impl = map[string]Impl{}
|
||||
regLock sync.RWMutex
|
||||
)
|
||||
|
||||
func Register(name string, impl Impl) {
|
||||
regLock.Lock()
|
||||
defer regLock.Unlock()
|
||||
|
||||
registry[name] = impl
|
||||
}
|
||||
|
||||
func Get(name string) (Impl, bool) {
|
||||
regLock.RLock()
|
||||
defer regLock.RUnlock()
|
||||
result, ok := registry[name]
|
||||
return result, ok
|
||||
}
|
||||
|
||||
func Methods() []string {
|
||||
regLock.RLock()
|
||||
defer regLock.RUnlock()
|
||||
var result []string
|
||||
for method := range registry {
|
||||
result = append(result, method)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
type IssueInput struct {
|
||||
Impressum *config.Impressum
|
||||
Rule *policy.Bot
|
||||
Challenge *Challenge
|
||||
OGTags map[string]string
|
||||
Store store.Interface
|
||||
}
|
||||
|
||||
type ValidateInput struct {
|
||||
Rule *policy.Bot
|
||||
Challenge *Challenge
|
||||
Store store.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(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
|
||||
}
|
||||
@@ -31,11 +31,11 @@ func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *challenge.IssueInput)
|
||||
|
||||
q := u.Query()
|
||||
q.Set("redir", r.URL.String())
|
||||
q.Set("challenge", in.Challenge)
|
||||
q.Set("challenge", in.Challenge.RandomData)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
loc := localization.GetLocalizer(r)
|
||||
component, err := web.BaseWithChallengeAndOGTags(loc.T("making_sure_not_bot"), page(in.Challenge, u.String(), in.Rule.Challenge.Difficulty, loc), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags, loc)
|
||||
component, err := web.BaseWithChallengeAndOGTags(loc.T("making_sure_not_bot"), page(u.String(), in.Rule.Challenge.Difficulty, loc), in.Impressum, in.Challenge.RandomData, in.Rule.Challenge, in.OGTags, loc)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't render page: %w", err)
|
||||
@@ -45,12 +45,10 @@ func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *challenge.IssueInput)
|
||||
}
|
||||
|
||||
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
|
||||
wantChallenge := in.Challenge
|
||||
|
||||
gotChallenge := r.FormValue("challenge")
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(wantChallenge), []byte(gotChallenge)) != 1 {
|
||||
return challenge.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", challenge.ErrFailed, wantChallenge, gotChallenge))
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/TecharoHQ/anubis/lib/localization"
|
||||
)
|
||||
|
||||
templ page(challenge, redir string, difficulty int, loc *localization.SimpleLocalizer) {
|
||||
templ page(redir string, difficulty int, loc *localization.SimpleLocalizer) {
|
||||
<div class="centered-div">
|
||||
<img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/>
|
||||
<img style="display:none;" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/>
|
||||
|
||||
4
lib/challenge/metarefresh/metarefresh_templ.go
generated
4
lib/challenge/metarefresh/metarefresh_templ.go
generated
@@ -1,6 +1,6 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.898
|
||||
// templ: version: v0.3.906
|
||||
package metarefresh
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/TecharoHQ/anubis/lib/localization"
|
||||
)
|
||||
|
||||
func page(challenge, redir string, difficulty int, loc *localization.SimpleLocalizer) templ.Component {
|
||||
func page(redir string, difficulty int, loc *localization.SimpleLocalizer) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||
chall "github.com/TecharoHQ/anubis/lib/challenge"
|
||||
"github.com/TecharoHQ/anubis/lib/localization"
|
||||
"github.com/TecharoHQ/anubis/web"
|
||||
@@ -31,7 +30,7 @@ func (i *Impl) Setup(mux *http.ServeMux) {
|
||||
|
||||
func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
|
||||
loc := localization.GetLocalizer(r)
|
||||
component, err := web.BaseWithChallengeAndOGTags(loc.T("making_sure_not_bot"), web.Index(loc), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags, loc)
|
||||
component, err := web.BaseWithChallengeAndOGTags(loc.T("making_sure_not_bot"), web.Index(loc), in.Impressum, in.Challenge.RandomData, in.Rule.Challenge, in.OGTags, loc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't render page: %w", err)
|
||||
}
|
||||
@@ -39,9 +38,9 @@ func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *chall.IssueInput) (te
|
||||
return component, nil
|
||||
}
|
||||
|
||||
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
|
||||
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
|
||||
rule := in.Rule
|
||||
challenge := in.Challenge
|
||||
challenge := in.Challenge.RandomData
|
||||
|
||||
nonceStr := r.FormValue("nonce")
|
||||
if nonceStr == "" {
|
||||
|
||||
@@ -127,15 +127,22 @@ func TestBasic(t *testing.T) {
|
||||
i.Setup(http.NewServeMux())
|
||||
|
||||
inp := &challenge.IssueInput{
|
||||
Rule: bot,
|
||||
Challenge: cs.challengeStr,
|
||||
Rule: bot,
|
||||
Challenge: &challenge.Challenge{
|
||||
RandomData: cs.challengeStr,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := i.Issue(cs.req, lg, inp); err != nil {
|
||||
t.Errorf("can't issue challenge: %v", err)
|
||||
}
|
||||
|
||||
if err := i.Validate(cs.req, lg, &challenge.ValidateInput{Rule: bot, Challenge: cs.challengeStr}); !errors.Is(err, cs.err) {
|
||||
if err := i.Validate(cs.req, lg, &challenge.ValidateInput{
|
||||
Rule: bot,
|
||||
Challenge: &challenge.Challenge{
|
||||
RandomData: cs.challengeStr,
|
||||
},
|
||||
}); !errors.Is(err, cs.err) {
|
||||
t.Errorf("got wrong error from Validate, got %v but wanted %v", err, cs.err)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user