mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-12 19:48:44 +00:00
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||
"github.com/TecharoHQ/anubis/internal/store/valkey"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
)
|
||||
@@ -68,6 +69,7 @@ type Server struct {
|
||||
pub ed25519.PublicKey
|
||||
opts Options
|
||||
cookieName string
|
||||
store *valkey.Store
|
||||
}
|
||||
|
||||
func (s *Server) challengeFor(r *http.Request, difficulty int) string {
|
||||
@@ -233,6 +235,10 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
lg = lg.With("check_result", cr)
|
||||
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||
|
||||
if s.store != nil {
|
||||
s.store.Increment(r.Context(), []string{"pass_rate", "User-Agent", r.UserAgent(), "challenges_issued"})
|
||||
}
|
||||
|
||||
err = encoder.Encode(struct {
|
||||
Rules *config.ChallengeRules `json:"rules"`
|
||||
Challenge string `json:"challenge"`
|
||||
@@ -325,6 +331,9 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("hash does not match", "got", response, "want", calculated)
|
||||
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden)
|
||||
if s.store != nil {
|
||||
s.store.Increment(r.Context(), []string{"pass_rate", "User-Agent", r.UserAgent(), "fail"})
|
||||
}
|
||||
failedValidations.Inc()
|
||||
return
|
||||
}
|
||||
@@ -334,6 +343,9 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty)
|
||||
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden)
|
||||
if s.store != nil {
|
||||
s.store.Increment(r.Context(), []string{"pass_rate", "User-Agent", r.UserAgent(), "fail"})
|
||||
}
|
||||
failedValidations.Inc()
|
||||
return
|
||||
}
|
||||
@@ -370,6 +382,10 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
Path: cookiePath,
|
||||
})
|
||||
|
||||
if s.store != nil {
|
||||
s.store.Increment(r.Context(), []string{"pass_rate", "User-Agent", r.UserAgent(), "pass"})
|
||||
}
|
||||
|
||||
challengesValidated.Inc()
|
||||
lg.Debug("challenge passed, redirecting to app")
|
||||
http.Redirect(w, r, redir, http.StatusFound)
|
||||
@@ -399,6 +415,8 @@ func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error)
|
||||
return decaymap.Zilch[policy.CheckResult](), nil, fmt.Errorf("[misconfiguration] %q is not an IP address", host)
|
||||
}
|
||||
|
||||
weight := 0
|
||||
|
||||
for _, b := range s.policy.Bots {
|
||||
match, err := b.Rules.Check(r)
|
||||
if err != nil {
|
||||
@@ -406,10 +424,27 @@ func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error)
|
||||
}
|
||||
|
||||
if match {
|
||||
return cr("bot/"+b.Name, b.Action), &b, nil
|
||||
switch b.Action {
|
||||
case config.RuleDeny, config.RuleAllow, config.RuleBenchmark:
|
||||
return cr("bot/"+b.Name, b.Action), &b, nil
|
||||
case config.RuleChallenge:
|
||||
weight += 5
|
||||
case config.RuleWeigh:
|
||||
weight += b.Weight.Adjust
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if weight < 0 {
|
||||
return cr("weight/okay", config.RuleAllow), &policy.Bot{
|
||||
Challenge: &config.ChallengeRules{
|
||||
Difficulty: s.policy.DefaultDifficulty,
|
||||
ReportAs: s.policy.DefaultDifficulty,
|
||||
Algorithm: config.AlgorithmFast,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return cr("default/allow", config.RuleAllow), &policy.Bot{
|
||||
Challenge: &config.ChallengeRules{
|
||||
Difficulty: s.policy.DefaultDifficulty,
|
||||
|
||||
@@ -65,6 +65,10 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
||||
return
|
||||
}
|
||||
|
||||
if s.store != nil {
|
||||
s.store.Increment(r.Context(), []string{"pass_rate", "User-Agent", r.UserAgent(), "challenges_issued"})
|
||||
}
|
||||
|
||||
handler := internal.NoStoreCache(templ.Handler(
|
||||
component,
|
||||
templ.WithStatus(s.opts.Policy.StatusCodes.Challenge),
|
||||
|
||||
@@ -12,6 +12,7 @@ type Bot struct {
|
||||
Challenge *config.ChallengeRules
|
||||
Name string
|
||||
Action config.Rule
|
||||
Weight *config.Weight
|
||||
}
|
||||
|
||||
func (b Bot) Hash() string {
|
||||
|
||||
@@ -7,12 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type CheckResult struct {
|
||||
Name string
|
||||
Rule config.Rule
|
||||
Name string
|
||||
Rule config.Rule
|
||||
Weight int
|
||||
}
|
||||
|
||||
func (cr CheckResult) LogValue() slog.Value {
|
||||
return slog.GroupValue(
|
||||
slog.String("name", cr.Name),
|
||||
slog.String("rule", string(cr.Rule)))
|
||||
slog.String("rule", string(cr.Rule)),
|
||||
slog.Int("weight", cr.Weight),
|
||||
)
|
||||
}
|
||||
|
||||
47
lib/policy/passratechecker.go
Normal file
47
lib/policy/passratechecker.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/TecharoHQ/anubis/internal/store/valkey"
|
||||
)
|
||||
|
||||
type PassRateChecker struct {
|
||||
store *valkey.Store
|
||||
header string
|
||||
rate float64
|
||||
}
|
||||
|
||||
func NewPassRateChecker(store *valkey.Store, rate float64) Checker {
|
||||
return &PassRateChecker{
|
||||
store: store,
|
||||
rate: rate,
|
||||
header: "User-Agent",
|
||||
}
|
||||
}
|
||||
|
||||
func (prc *PassRateChecker) Hash() string {
|
||||
return internal.SHA256sum(fmt.Sprintf("pass rate checker::%s", prc.header))
|
||||
}
|
||||
|
||||
func (prc *PassRateChecker) Check(r *http.Request) (bool, error) {
|
||||
data, err := prc.store.MultiGetInt(r.Context(), [][]string{
|
||||
{"pass_rate", prc.header, r.Header.Get(prc.header), "pass"},
|
||||
{"pass_rate", prc.header, r.Header.Get(prc.header), "challenges_issued"},
|
||||
{"pass_rate", prc.header, r.Header.Get(prc.header), "fail"},
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
passCount, challengeCount, failCount := data[0], data[1], data[2]
|
||||
passRate := float64(passCount-failCount) / float64(challengeCount)
|
||||
|
||||
if passRate >= prc.rate {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
@@ -116,6 +116,10 @@ func ParseConfig(fin io.Reader, fname string, defaultDifficulty int) (*ParsedCon
|
||||
}
|
||||
}
|
||||
|
||||
if b.Weight != nil {
|
||||
parsedBot.Weight = b.Weight
|
||||
}
|
||||
|
||||
parsedBot.Rules = cl
|
||||
|
||||
result.Bots = append(result.Bots, parsedBot)
|
||||
|
||||
Reference in New Issue
Block a user