mirror of
https://github.com/TecharoHQ/anubis.git
synced 2025-09-07 19:54:56 -04:00
feat: implement challenge registry (#607)
* feat: implement challenge method registry This paves the way for implementing a no-js check method (#95) by making the challenge providers more generic. Signed-off-by: Xe Iaso <me@xeiaso.net> * fix(lib/challenge): rename proof-of-work package to proofofwork Signed-off-by: Xe Iaso <me@xeiaso.net> * fix(lib): make validated challenges a CounterVec Signed-off-by: Xe Iaso <me@xeiaso.net> * fix(lib): annotate jwts with challenge method Signed-off-by: Xe Iaso <me@xeiaso.net> * test(lib/challenge/proofofwork): implement tests Signed-off-by: Xe Iaso <me@xeiaso.net> * test(lib): add smoke tests for known good and known bad config files Signed-off-by: Xe Iaso <me@xeiaso.net> * docs: update CHANGELOG Signed-off-by: Xe Iaso <me@xeiaso.net> * fix(lib): use challenge.Impl#Issue when issuing challenges Signed-off-by: Xe Iaso <me@xeiaso.net> --------- Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
parent
ba4412c907
commit
f2db43ad4b
@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Refactor challenge presentation logic to use a challenge registry
|
||||||
|
|
||||||
## v1.19.1: Jenomis cen Lexentale - Echo 1
|
## v1.19.1: Jenomis cen Lexentale - Echo 1
|
||||||
|
|
||||||
Return `data/bots/ai-robots-txt.yaml` to avoid breaking configs [#599](https://github.com/TecharoHQ/anubis/issues/599)
|
Return `data/bots/ai-robots-txt.yaml` to avoid breaking configs [#599](https://github.com/TecharoHQ/anubis/issues/599)
|
||||||
|
129
lib/anubis.go
129
lib/anubis.go
@ -3,16 +3,14 @@ package lib
|
|||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -26,8 +24,12 @@ import (
|
|||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy"
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||||
|
|
||||||
|
// challenge implementations
|
||||||
|
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -36,26 +38,20 @@ var (
|
|||||||
Help: "The total number of challenges issued",
|
Help: "The total number of challenges issued",
|
||||||
}, []string{"method"})
|
}, []string{"method"})
|
||||||
|
|
||||||
challengesValidated = promauto.NewCounter(prometheus.CounterOpts{
|
challengesValidated = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_challenges_validated",
|
Name: "anubis_challenges_validated",
|
||||||
Help: "The total number of challenges validated",
|
Help: "The total number of challenges validated",
|
||||||
})
|
}, []string{"method"})
|
||||||
|
|
||||||
droneBLHits = promauto.NewCounterVec(prometheus.CounterOpts{
|
droneBLHits = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_dronebl_hits",
|
Name: "anubis_dronebl_hits",
|
||||||
Help: "The total number of hits from DroneBL",
|
Help: "The total number of hits from DroneBL",
|
||||||
}, []string{"status"})
|
}, []string{"status"})
|
||||||
|
|
||||||
failedValidations = promauto.NewCounter(prometheus.CounterOpts{
|
failedValidations = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_failed_validations",
|
Name: "anubis_failed_validations",
|
||||||
Help: "The total number of failed validations",
|
Help: "The total number of failed validations",
|
||||||
})
|
}, []string{"method"})
|
||||||
|
|
||||||
timeTaken = promauto.NewHistogram(prometheus.HistogramOpts{
|
|
||||||
Name: "anubis_time_taken",
|
|
||||||
Help: "The time taken for a browser to generate a response (milliseconds)",
|
|
||||||
Buckets: prometheus.ExponentialBucketsRange(1, math.Pow(2, 18), 19),
|
|
||||||
})
|
|
||||||
|
|
||||||
requestsProxied = promauto.NewCounterVec(prometheus.CounterOpts{
|
requestsProxied = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_proxied_requests_total",
|
Name: "anubis_proxied_requests_total",
|
||||||
@ -320,6 +316,14 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
|
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := r.Cookie(anubis.TestCookieName); err == http.ErrNoCookie {
|
||||||
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
|
s.ClearCookie(w, anubis.TestCookieName, "/")
|
||||||
|
lg.Warn("user has cookies disabled, this is not an anubis bug")
|
||||||
|
s.respondWithError(w, r, "Your browser is configured to disable cookies. Anubis requires cookies for the legitimate interest of making sure you are a valid client. Please enable cookies for this domain")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s.ClearCookie(w, anubis.TestCookieName, "/")
|
s.ClearCookie(w, anubis.TestCookieName, "/")
|
||||||
|
|
||||||
redir := r.FormValue("redir")
|
redir := r.FormValue("redir")
|
||||||
@ -332,42 +336,6 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
// used by the path checker rule
|
// used by the path checker rule
|
||||||
r.URL = redirURL
|
r.URL = redirURL
|
||||||
|
|
||||||
cr, rule, err := s.check(r)
|
|
||||||
if err != nil {
|
|
||||||
lg.Error("check failed", "err", err)
|
|
||||||
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\".")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lg = lg.With("check_result", cr)
|
|
||||||
|
|
||||||
nonceStr := r.FormValue("nonce")
|
|
||||||
if nonceStr == "" {
|
|
||||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
|
||||||
lg.Debug("no nonce")
|
|
||||||
s.respondWithError(w, r, "missing nonce")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsedTimeStr := r.FormValue("elapsedTime")
|
|
||||||
if elapsedTimeStr == "" {
|
|
||||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
|
||||||
lg.Debug("no elapsedTime")
|
|
||||||
s.respondWithError(w, r, "missing elapsedTime")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
|
||||||
lg.Debug("elapsedTime doesn't parse", "err", err)
|
|
||||||
s.respondWithError(w, r, "invalid elapsedTime")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lg.Info("challenge took", "elapsedTime", elapsedTime)
|
|
||||||
timeTaken.Observe(elapsedTime)
|
|
||||||
|
|
||||||
response := r.FormValue("response")
|
|
||||||
urlParsed, err := r.URL.Parse(redir)
|
urlParsed, err := r.URL.Parse(redir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.respondWithError(w, r, "Redirect URL not parseable")
|
s.respondWithError(w, r, "Redirect URL not parseable")
|
||||||
@ -378,49 +346,44 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
cr, rule, err := s.check(r)
|
||||||
|
|
||||||
if _, err := r.Cookie(anubis.TestCookieName); err == http.ErrNoCookie {
|
|
||||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
|
||||||
s.ClearCookie(w, anubis.TestCookieName, cookiePath)
|
|
||||||
lg.Warn("user has cookies disabled, this is not an anubis bug")
|
|
||||||
s.respondWithError(w, r, "Your browser is configured to disable cookies. Anubis requires cookies for the legitimate interest of making sure you are a valid client. Please enable cookies for this domain")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce, err := strconv.Atoi(nonceStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
lg.Error("check failed", "err", err)
|
||||||
lg.Debug("nonce doesn't parse", "err", err)
|
s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\"")
|
||||||
s.respondWithError(w, r, "invalid response")
|
return
|
||||||
|
}
|
||||||
|
lg = lg.With("check_result", cr)
|
||||||
|
|
||||||
|
impl, ok := challenge.Get(rule.Challenge.Algorithm)
|
||||||
|
if !ok {
|
||||||
|
lg.Error("check failed", "err", err)
|
||||||
|
s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
calcString := fmt.Sprintf("%s%d", challenge, nonce)
|
challengeStr := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||||
calculated := internal.SHA256sum(calcString)
|
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
|
if err := impl.Validate(r, lg, rule, challengeStr); err != nil {
|
||||||
|
failedValidations.WithLabelValues(string(rule.Challenge.Algorithm)).Inc()
|
||||||
|
var cerr *challenge.Error
|
||||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
s.ClearCookie(w, s.cookieName, cookiePath)
|
||||||
lg.Debug("hash does not match", "got", response, "want", calculated)
|
lg.Debug("challenge validate call failed", "err", err)
|
||||||
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden)
|
|
||||||
failedValidations.Inc()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare the leading zeroes
|
switch {
|
||||||
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
|
case errors.As(err, &cerr):
|
||||||
s.ClearCookie(w, s.cookieName, cookiePath)
|
switch {
|
||||||
lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty)
|
case errors.Is(err, challenge.ErrFailed):
|
||||||
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden)
|
s.respondWithStatus(w, r, cerr.PublicReason, cerr.StatusCode)
|
||||||
failedValidations.Inc()
|
case errors.Is(err, challenge.ErrInvalidFormat), errors.Is(err, challenge.ErrMissingField):
|
||||||
return
|
s.respondWithError(w, r, cerr.PublicReason)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate JWT cookie
|
// generate JWT cookie
|
||||||
tokenString, err := s.signJWT(jwt.MapClaims{
|
tokenString, err := s.signJWT(jwt.MapClaims{
|
||||||
"challenge": challenge,
|
"challenge": challengeStr,
|
||||||
"nonce": nonceStr,
|
"method": rule.Challenge.Algorithm,
|
||||||
"response": response,
|
|
||||||
"policyRule": rule.Hash(),
|
"policyRule": rule.Hash(),
|
||||||
"action": string(cr.Rule),
|
"action": string(cr.Rule),
|
||||||
})
|
})
|
||||||
@ -433,7 +396,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
s.SetCookie(w, s.cookieName, tokenString, cookiePath)
|
s.SetCookie(w, s.cookieName, tokenString, cookiePath)
|
||||||
|
|
||||||
challengesValidated.Inc()
|
challengesValidated.WithLabelValues(rule.Challenge.Algorithm).Inc()
|
||||||
lg.Debug("challenge passed, redirecting to app")
|
lg.Debug("challenge passed, redirecting to app")
|
||||||
http.Redirect(w, r, redir, http.StatusFound)
|
http.Redirect(w, r, redir, http.StatusFound)
|
||||||
}
|
}
|
||||||
@ -477,7 +440,7 @@ func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error)
|
|||||||
Challenge: &config.ChallengeRules{
|
Challenge: &config.ChallengeRules{
|
||||||
Difficulty: s.policy.DefaultDifficulty,
|
Difficulty: s.policy.DefaultDifficulty,
|
||||||
ReportAs: s.policy.DefaultDifficulty,
|
ReportAs: s.policy.DefaultDifficulty,
|
||||||
Algorithm: config.AlgorithmFast,
|
Algorithm: config.DefaultAlgorithm,
|
||||||
},
|
},
|
||||||
Rules: &policy.CheckerList{},
|
Rules: &policy.CheckerList{},
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -45,11 +45,11 @@ func spawnAnubis(t *testing.T, opts Options) *Server {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
type challenge struct {
|
type challengeResp struct {
|
||||||
Challenge string `json:"challenge"`
|
Challenge string `json:"challenge"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeChallenge(t *testing.T, ts *httptest.Server, cli *http.Client) challenge {
|
func makeChallenge(t *testing.T, ts *httptest.Server, cli *http.Client) challengeResp {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", nil)
|
req, err := http.NewRequest(http.MethodPost, ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", nil)
|
||||||
@ -67,7 +67,7 @@ func makeChallenge(t *testing.T, ts *httptest.Server, cli *http.Client) challeng
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var chall challenge
|
var chall challengeResp
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
||||||
t.Fatalf("can't read challenge response body: %v", err)
|
t.Fatalf("can't read challenge response body: %v", err)
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ func makeChallenge(t *testing.T, ts *httptest.Server, cli *http.Client) challeng
|
|||||||
return chall
|
return chall
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleChallengeZeroDifficulty(t *testing.T, ts *httptest.Server, cli *http.Client, chall challenge) *http.Response {
|
func handleChallengeZeroDifficulty(t *testing.T, ts *httptest.Server, cli *http.Client, chall challengeResp) *http.Response {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
nonce := 0
|
nonce := 0
|
||||||
@ -420,7 +420,7 @@ func TestBasePrefix(t *testing.T) {
|
|||||||
t.Errorf("expected status code %d, got: %d", http.StatusOK, resp.StatusCode)
|
t.Errorf("expected status code %d, got: %d", http.StatusOK, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var chall challenge
|
var chall challengeResp
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
||||||
t.Fatalf("can't read challenge response body: %v", err)
|
t.Fatalf("can't read challenge response body: %v", err)
|
||||||
}
|
}
|
||||||
|
47
lib/challenge/challenge.go
Normal file
47
lib/challenge/challenge.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package challenge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
"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 Impl interface {
|
||||||
|
Fail(w http.ResponseWriter, r *http.Request) error
|
||||||
|
Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string, ogTags map[string]string) (templ.Component, error)
|
||||||
|
Validate(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string) error
|
||||||
|
}
|
37
lib/challenge/error.go
Normal file
37
lib/challenge/error.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package challenge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Verb string
|
||||||
|
PublicReason string
|
||||||
|
PrivateReason error
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return fmt.Sprintf("challenge: error when processing challenge: %s: %v", e.Verb, e.PrivateReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Unwrap() error {
|
||||||
|
return e.PrivateReason
|
||||||
|
}
|
14
lib/challenge/metrics.go
Normal file
14
lib/challenge/metrics.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package challenge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var TimeTaken = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
|
Name: "anubis_time_taken",
|
||||||
|
Help: "The time taken for a browser to generate a response (milliseconds)",
|
||||||
|
Buckets: prometheus.ExponentialBucketsRange(1, math.Pow(2, 20), 20),
|
||||||
|
}, []string{"method"})
|
83
lib/challenge/proofofwork/proofofwork.go
Normal file
83
lib/challenge/proofofwork/proofofwork.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package proofofwork
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
chall "github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
"github.com/TecharoHQ/anubis/web"
|
||||||
|
"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) Fail(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Impl) Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string, ogTags map[string]string) (templ.Component, error) {
|
||||||
|
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't render page: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return component, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string) error {
|
||||||
|
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))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsedTimeStr := r.FormValue("elapsedTime")
|
||||||
|
if elapsedTimeStr == "" {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w elapsedTime", chall.ErrMissingField))
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: elapsedTime: %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))
|
||||||
|
}
|
||||||
|
|
||||||
|
lg.Debug("challenge took", "elapsedTime", elapsedTime)
|
||||||
|
chall.TimeTaken.WithLabelValues(i.Algorithm).Observe(elapsedTime)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
136
lib/challenge/proofofwork/proofofwork_test.go
Normal file
136
lib/challenge/proofofwork/proofofwork_test.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package proofofwork
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mkRequest(t *testing.T, values map[string]string) *http.Request {
|
||||||
|
t.Helper()
|
||||||
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
|
||||||
|
for k, v := range values {
|
||||||
|
q.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasic(t *testing.T) {
|
||||||
|
i := &Impl{Algorithm: "fast"}
|
||||||
|
bot := &policy.Bot{
|
||||||
|
Challenge: &config.ChallengeRules{
|
||||||
|
Algorithm: "fast",
|
||||||
|
Difficulty: 0,
|
||||||
|
ReportAs: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const challengeStr = "hunter"
|
||||||
|
const response = "2652bdba8fb4d2ab39ef28d8534d7694c557a4ae146c1e9237bd8d950280500e"
|
||||||
|
|
||||||
|
for _, cs := range []struct {
|
||||||
|
name string
|
||||||
|
req *http.Request
|
||||||
|
err error
|
||||||
|
challengeStr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "allgood",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: nil,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-params",
|
||||||
|
req: mkRequest(t, map[string]string{}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing-nonce",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing-elapsedTime",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing-response",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong-nonce-format",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "taco",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrInvalidFormat,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong-elapsedTime-format",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "taco",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrInvalidFormat,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-response",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrFailed,
|
||||||
|
challengeStr: "Tacos are tasty",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(cs.name, func(t *testing.T) {
|
||||||
|
lg := slog.With()
|
||||||
|
|
||||||
|
if _, err := i.Issue(cs.req, lg, bot, cs.challengeStr, nil); err != nil {
|
||||||
|
t.Errorf("can't issue challenge: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Validate(cs.req, lg, bot, cs.challengeStr); !errors.Is(err, cs.err) {
|
||||||
|
t.Errorf("got wrong error from Validate, got %v but wanted %v", err, cs.err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ package lib
|
|||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@ -17,6 +18,7 @@ import (
|
|||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy"
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
"github.com/TecharoHQ/anubis/web"
|
"github.com/TecharoHQ/anubis/web"
|
||||||
"github.com/TecharoHQ/anubis/xess"
|
"github.com/TecharoHQ/anubis/xess"
|
||||||
@ -65,6 +67,17 @@ func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedC
|
|||||||
}(fin)
|
}(fin)
|
||||||
|
|
||||||
anubisPolicy, err := policy.ParseConfig(fin, fname, defaultDifficulty)
|
anubisPolicy, err := policy.ParseConfig(fin, fname, defaultDifficulty)
|
||||||
|
var validationErrs []error
|
||||||
|
|
||||||
|
for _, b := range anubisPolicy.Bots {
|
||||||
|
if _, ok := challenge.Get(b.Challenge.Algorithm); !ok {
|
||||||
|
validationErrs = append(validationErrs, fmt.Errorf("%w %s", policy.ErrChallengeRuleHasWrongAlgorithm, b.Challenge.Algorithm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(validationErrs) != 0 {
|
||||||
|
return nil, fmt.Errorf("can't do final validation of Anubis config: %w", errors.Join(validationErrs...))
|
||||||
|
}
|
||||||
|
|
||||||
return anubisPolicy, err
|
return anubisPolicy, err
|
||||||
}
|
}
|
||||||
|
51
lib/config_test.go
Normal file
51
lib/config_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInvalidChallengeMethod(t *testing.T) {
|
||||||
|
if _, err := LoadPoliciesOrDefault("testdata/invalid-challenge-method.yaml", 4); !errors.Is(err, policy.ErrChallengeRuleHasWrongAlgorithm) {
|
||||||
|
t.Fatalf("wanted error %v but got %v", policy.ErrChallengeRuleHasWrongAlgorithm, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadConfigs(t *testing.T) {
|
||||||
|
finfos, err := os.ReadDir("policy/config/testdata/bad")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, st := range finfos {
|
||||||
|
st := st
|
||||||
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
|
if _, err := LoadPoliciesOrDefault(filepath.Join("policy", "config", "testdata", "good", st.Name()), anubis.DefaultDifficulty); err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoodConfigs(t *testing.T) {
|
||||||
|
finfos, err := os.ReadDir("policy/config/testdata/good")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, st := range finfos {
|
||||||
|
st := st
|
||||||
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
|
if _, err := LoadPoliciesOrDefault(filepath.Join("policy", "config", "testdata", "good", st.Name()), anubis.DefaultDifficulty); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
17
lib/http.go
17
lib/http.go
@ -1,6 +1,7 @@
|
|||||||
package lib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
@ -9,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TecharoHQ/anubis"
|
"github.com/TecharoHQ/anubis"
|
||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy"
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
"github.com/TecharoHQ/anubis/web"
|
"github.com/TecharoHQ/anubis/web"
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
@ -75,7 +77,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
|||||||
}
|
}
|
||||||
|
|
||||||
challengesIssued.WithLabelValues("embedded").Add(1)
|
challengesIssued.WithLabelValues("embedded").Add(1)
|
||||||
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
challengeStr := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||||
|
|
||||||
var ogTags map[string]string = nil
|
var ogTags map[string]string = nil
|
||||||
if s.opts.OGPassthrough {
|
if s.opts.OGPassthrough {
|
||||||
@ -88,14 +90,21 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
|||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: anubis.TestCookieName,
|
Name: anubis.TestCookieName,
|
||||||
Value: challenge,
|
Value: challengeStr,
|
||||||
Expires: time.Now().Add(30 * time.Minute),
|
Expires: time.Now().Add(30 * time.Minute),
|
||||||
Path: "/",
|
Path: "/",
|
||||||
})
|
})
|
||||||
|
|
||||||
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags)
|
impl, ok := challenge.Get(rule.Challenge.Algorithm)
|
||||||
|
if !ok {
|
||||||
|
lg.Error("check failed", "err", "can't get algorithm", "algorithm", rule.Challenge.Algorithm)
|
||||||
|
s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
component, err := impl.Issue(r, lg, rule, challengeStr, ogTags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this.
|
lg.Error("[unexpected] render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this.
|
||||||
s.respondWithError(w, r, "Internal Server Error: please contact the administrator and ask them to look for the logs around \"RenderIndex\"")
|
s.respondWithError(w, r, "Internal Server Error: please contact the administrator and ask them to look for the logs around \"RenderIndex\"")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -42,13 +42,7 @@ const (
|
|||||||
RuleBenchmark Rule = "DEBUG_BENCHMARK"
|
RuleBenchmark Rule = "DEBUG_BENCHMARK"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Algorithm string
|
const DefaultAlgorithm = "fast"
|
||||||
|
|
||||||
const (
|
|
||||||
AlgorithmUnknown Algorithm = ""
|
|
||||||
AlgorithmFast Algorithm = "fast"
|
|
||||||
AlgorithmSlow Algorithm = "slow"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BotConfig struct {
|
type BotConfig struct {
|
||||||
UserAgentRegex *string `json:"user_agent_regex"`
|
UserAgentRegex *string `json:"user_agent_regex"`
|
||||||
@ -170,15 +164,14 @@ func (b BotConfig) Valid() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChallengeRules struct {
|
type ChallengeRules struct {
|
||||||
Algorithm Algorithm `json:"algorithm"`
|
Algorithm string `json:"algorithm"`
|
||||||
Difficulty int `json:"difficulty"`
|
Difficulty int `json:"difficulty"`
|
||||||
ReportAs int `json:"report_as"`
|
ReportAs int `json:"report_as"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid")
|
ErrChallengeDifficultyTooLow = errors.New("config.Bot.ChallengeRules: difficulty is too low (must be >= 1)")
|
||||||
ErrChallengeDifficultyTooLow = errors.New("config.Bot.ChallengeRules: difficulty is too low (must be >= 1)")
|
ErrChallengeDifficultyTooHigh = errors.New("config.Bot.ChallengeRules: difficulty is too high (must be <= 64)")
|
||||||
ErrChallengeDifficultyTooHigh = errors.New("config.Bot.ChallengeRules: difficulty is too high (must be <= 64)")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (cr ChallengeRules) Valid() error {
|
func (cr ChallengeRules) Valid() error {
|
||||||
@ -192,13 +185,6 @@ func (cr ChallengeRules) Valid() error {
|
|||||||
errs = append(errs, fmt.Errorf("%w, got: %d", ErrChallengeDifficultyTooHigh, cr.Difficulty))
|
errs = append(errs, fmt.Errorf("%w, got: %d", ErrChallengeDifficultyTooHigh, cr.Difficulty))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch cr.Algorithm {
|
|
||||||
case AlgorithmFast, AlgorithmSlow, AlgorithmUnknown:
|
|
||||||
// do nothing, it's all good
|
|
||||||
default:
|
|
||||||
errs = append(errs, fmt.Errorf("%w: %q", ErrChallengeRuleHasWrongAlgorithm, cr.Algorithm))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
return fmt.Errorf("config: challenge rules entry is not valid:\n%w", errors.Join(errs...))
|
return fmt.Errorf("config: challenge rules entry is not valid:\n%w", errors.Join(errs...))
|
||||||
}
|
}
|
||||||
|
@ -130,20 +130,6 @@ func TestBotValid(t *testing.T) {
|
|||||||
},
|
},
|
||||||
err: ErrChallengeDifficultyTooHigh,
|
err: ErrChallengeDifficultyTooHigh,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "challenge wrong algorithm",
|
|
||||||
bot: BotConfig{
|
|
||||||
Name: "mozilla-ua",
|
|
||||||
Action: RuleChallenge,
|
|
||||||
PathRegex: p("Mozilla"),
|
|
||||||
Challenge: &ChallengeRules{
|
|
||||||
Difficulty: 420,
|
|
||||||
ReportAs: 4,
|
|
||||||
Algorithm: "high quality rips",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
err: ErrChallengeRuleHasWrongAlgorithm,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "invalid cidr range",
|
name: "invalid cidr range",
|
||||||
bot: BotConfig{
|
bot: BotConfig{
|
||||||
@ -361,7 +347,7 @@ func TestBotConfigZero(t *testing.T) {
|
|||||||
b.Challenge = &ChallengeRules{
|
b.Challenge = &ChallengeRules{
|
||||||
Difficulty: 4,
|
Difficulty: 4,
|
||||||
ReportAs: 4,
|
ReportAs: 4,
|
||||||
Algorithm: AlgorithmFast,
|
Algorithm: DefaultAlgorithm,
|
||||||
}
|
}
|
||||||
if b.Zero() {
|
if b.Zero() {
|
||||||
t.Error("BotConfig with challenge rules is zero value")
|
t.Error("BotConfig with challenge rules is zero value")
|
||||||
|
@ -5,10 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
|
||||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -16,6 +15,8 @@ var (
|
|||||||
Name: "anubis_policy_results",
|
Name: "anubis_policy_results",
|
||||||
Help: "The results of each policy rule",
|
Help: "The results of each policy rule",
|
||||||
}, []string{"rule", "action"})
|
}, []string{"rule", "action"})
|
||||||
|
|
||||||
|
ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid")
|
||||||
)
|
)
|
||||||
|
|
||||||
type ParsedConfig struct {
|
type ParsedConfig struct {
|
||||||
@ -107,12 +108,12 @@ func ParseConfig(fin io.Reader, fname string, defaultDifficulty int) (*ParsedCon
|
|||||||
parsedBot.Challenge = &config.ChallengeRules{
|
parsedBot.Challenge = &config.ChallengeRules{
|
||||||
Difficulty: defaultDifficulty,
|
Difficulty: defaultDifficulty,
|
||||||
ReportAs: defaultDifficulty,
|
ReportAs: defaultDifficulty,
|
||||||
Algorithm: config.AlgorithmFast,
|
Algorithm: "fast",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
parsedBot.Challenge = b.Challenge
|
parsedBot.Challenge = b.Challenge
|
||||||
if parsedBot.Challenge.Algorithm == config.AlgorithmUnknown {
|
if parsedBot.Challenge.Algorithm == "" {
|
||||||
parsedBot.Challenge.Algorithm = config.AlgorithmFast
|
parsedBot.Challenge.Algorithm = config.DefaultAlgorithm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
9
lib/testdata/hack-test.json
vendored
Normal file
9
lib/testdata/hack-test.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "ipv6-ula",
|
||||||
|
"action": "ALLOW",
|
||||||
|
"remote_addresses": [
|
||||||
|
"fc00::/7"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
3
lib/testdata/hack-test.yaml
vendored
Normal file
3
lib/testdata/hack-test.yaml
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- name: well-known
|
||||||
|
path_regex: ^/.well-known/.*$
|
||||||
|
action: ALLOW
|
8
lib/testdata/invalid-challenge-method.yaml
vendored
Normal file
8
lib/testdata/invalid-challenge-method.yaml
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
bots:
|
||||||
|
- name: generic-bot-catchall
|
||||||
|
user_agent_regex: (?i:bot|crawler)
|
||||||
|
action: CHALLENGE
|
||||||
|
challenge:
|
||||||
|
difficulty: 16
|
||||||
|
report_as: 4
|
||||||
|
algorithm: hunter2 # invalid algorithm
|
Loading…
x
Reference in New Issue
Block a user