From f6481b81a28f6a7cd363b40aa300f895aba0406d Mon Sep 17 00:00:00 2001
From: Xe Iaso
Date: Mon, 4 Aug 2025 14:49:19 -0400
Subject: [PATCH] fix(web): embed challenge ID in pass-challenge invocations
(#944)
* refactor: make challenge pages return the challenge component
This means that challenge pages will return only the little bit that
actually matters, not the entire component.
Signed-off-by: Xe Iaso
* fix(web): move Anubis version info to be implicitly in the footer
Signed-off-by: Xe Iaso
* fix(web): embed challenge ID into generated pages
Signed-off-by: Xe Iaso
* fix(lib): make tests pass
Signed-off-by: Xe Iaso
* test(lib/policy/config): amend tests
Signed-off-by: Xe Iaso
* test(lib): fix tests again
Signed-off-by: Xe Iaso
---------
Signed-off-by: Xe Iaso
Signed-off-by: Xe Iaso
---
.github/actions/spelling/expect.txt | 3 +-
docs/docs/CHANGELOG.md | 2 +
lib/anubis.go | 13 +-
lib/anubis_test.go | 80 ++-
lib/challenge/metarefresh/metarefresh.go | 9 +-
lib/challenge/proofofwork/proofofwork.go | 14 +-
lib/challenge/proofofwork/proofofwork.templ | 39 ++
.../proofofwork/proofofwork_templ.go | 175 +++++
lib/http.go | 14 +-
lib/policy/config/config.go | 4 +-
lib/policy/config/config_test.go | 2 +-
lib/policy/config/threshold_test.go | 2 +-
lib/testdata/test_config.yaml | 9 +-
lib/testdata/test_config_no_thresholds.yaml | 38 ++
lib/testdata/zero_difficulty.yaml | 45 ++
web/index.go | 11 +-
web/index.templ | 39 +-
web/index_templ.go | 623 +++++++-----------
web/js/main.mjs | 4 +-
19 files changed, 629 insertions(+), 497 deletions(-)
create mode 100644 lib/challenge/proofofwork/proofofwork.templ
create mode 100644 lib/challenge/proofofwork/proofofwork_templ.go
create mode 100644 lib/testdata/test_config_no_thresholds.yaml
create mode 100644 lib/testdata/zero_difficulty.yaml
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
index e43c222..44642b7 100644
--- a/.github/actions/spelling/expect.txt
+++ b/.github/actions/spelling/expect.txt
@@ -52,7 +52,6 @@ checkresult
chibi
cidranger
ckie
-ckies
cloudflare
Codespaces
confd
@@ -297,6 +296,7 @@ thoth
thothmock
Tik
Timpibot
+TLog
traefik
uberspace
Unbreak
@@ -313,7 +313,6 @@ vendored
vhosts
VKE
Vultr
-waitloop
weblate
webmaster
webpage
diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md
index a684b5b..ae1f39c 100644
--- a/docs/docs/CHANGELOG.md
+++ b/docs/docs/CHANGELOG.md
@@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Legacy JavaScript code has been eliminated.
- The contact email in the LibreJS header has been changed.
- The hard dependency on WebCrypto has been removed, allowing a proof of work challenge to work over plain (unencrypted) HTTP.
+- Firefox for Android support has been fixed by embedding the challenge ID into the pass-challenge route. This also fixes some inconsistent issues with other mobile browsers.
+- The Anubis version number is put in the footer of every page.
- The legacy JSON based policy file example has been removed and all documentation for how to write a policy file in JSON has been deleted. JSON based policy files will still work, but YAML is the superior option for Anubis configuration.
- A standard library HTTP server log message about HTTP pipelining not working has been filtered out of Anubis' logs. There is no action that can be taken about it.
diff --git a/lib/anubis.go b/lib/anubis.go
index b9eba5f..d23df3f 100644
--- a/lib/anubis.go
+++ b/lib/anubis.go
@@ -92,15 +92,10 @@ func (s *Server) getTokenKeyfunc() jwt.Keyfunc {
}
func (s *Server) getChallenge(r *http.Request) (*challenge.Challenge, error) {
- ckies := r.CookiesNamed(anubis.TestCookieName)
- if len(ckies) == 0 {
- return nil, store.ErrNotFound
- }
-
+ id := r.FormValue("id")
j := store.JSON[challenge.Challenge]{Underlying: s.store}
- ckie := ckies[0]
- chall, err := j.Get(r.Context(), "challenge:"+ckie.Value)
+ chall, err := j.Get(r.Context(), "challenge:"+id)
return &chall, err
}
@@ -374,9 +369,11 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
err = encoder.Encode(struct {
Rules *config.ChallengeRules `json:"rules"`
Challenge string `json:"challenge"`
+ ID string `json:"id"`
}{
- Challenge: chall.RandomData,
Rules: rule.Challenge,
+ Challenge: chall.RandomData,
+ ID: chall.ID,
})
if err != nil {
lg.Error("failed to encode challenge", "err", err)
diff --git a/lib/anubis_test.go b/lib/anubis_test.go
index 903a467..0f6ef7f 100644
--- a/lib/anubis_test.go
+++ b/lib/anubis_test.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "log/slog"
"net/http"
"net/http/httptest"
"net/url"
@@ -22,8 +23,25 @@ import (
"github.com/TecharoHQ/anubis/lib/thoth/thothmock"
)
-func init() {
- internal.InitSlog("debug")
+// TLogWriter implements io.Writer by logging each line to t.Log.
+type TLogWriter struct {
+ t *testing.T
+}
+
+// NewTLogWriter returns an io.Writer that sends output to t.Log.
+func NewTLogWriter(t *testing.T) io.Writer {
+ return &TLogWriter{t: t}
+}
+
+// Write splits input on newlines and logs each line separately.
+func (w *TLogWriter) Write(p []byte) (n int, err error) {
+ lines := strings.Split(string(p), "\n")
+ for _, line := range lines {
+ if line != "" {
+ w.t.Log(line)
+ }
+ }
+ return len(p), nil
}
func loadPolicies(t *testing.T, fname string, difficulty int) *policy.ParsedConfig {
@@ -35,6 +53,8 @@ func loadPolicies(t *testing.T, fname string, difficulty int) *policy.ParsedConf
fname = "./testdata/test_config.yaml"
}
+ t.Logf("loading policy file: %s", fname)
+
anubisPolicy, err := LoadPoliciesOrDefault(ctx, fname, difficulty)
if err != nil {
t.Fatal(err)
@@ -55,10 +75,16 @@ func spawnAnubis(t *testing.T, opts Options) *Server {
t.Fatalf("can't construct libanubis.Server: %v", err)
}
+ s.logger = slog.New(slog.NewJSONHandler(&TLogWriter{t: t}, &slog.HandlerOptions{
+ AddSource: true,
+ Level: slog.LevelDebug,
+ }))
+
return s
}
type challengeResp struct {
+ ID string `json:"id"`
Challenge string `json:"challenge"`
}
@@ -91,6 +117,8 @@ func makeChallenge(t *testing.T, ts *httptest.Server, cli *http.Client) challeng
func handleChallengeZeroDifficulty(t *testing.T, ts *httptest.Server, cli *http.Client, chall challengeResp) *http.Response {
t.Helper()
+ t.Logf("%#v", chall)
+
nonce := 0
elapsedTime := 420
redir := "/"
@@ -108,8 +136,11 @@ func handleChallengeZeroDifficulty(t *testing.T, ts *httptest.Server, cli *http.
q.Set("nonce", fmt.Sprint(nonce))
q.Set("redir", redir)
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
+ q.Set("id", chall.ID)
req.URL.RawQuery = q.Encode()
+ t.Log(q.Encode())
+
resp, err := cli.Do(req)
if err != nil {
t.Fatalf("can't do request: %v", err)
@@ -155,6 +186,17 @@ func (lcj *loggingCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
lcj.cookies[u.Host] = append(lcj.cookies[u.Host], cookies...)
}
+type userAgentRoundTripper struct {
+ rt http.RoundTripper
+}
+
+func (u *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ // Only set if not already present
+ req = req.Clone(req.Context()) // avoid mutating original request
+ req.Header.Set("User-Agent", "Mozilla/5.0")
+ return u.rt.RoundTrip(req)
+}
+
func httpClient(t *testing.T) *http.Client {
t.Helper()
@@ -163,6 +205,9 @@ func httpClient(t *testing.T) *http.Client {
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
+ Transport: &userAgentRoundTripper{
+ rt: http.DefaultTransport,
+ },
}
return cli
@@ -207,7 +252,7 @@ func TestCVE2025_24369(t *testing.T) {
}
func TestCookieCustomExpiration(t *testing.T) {
- pol := loadPolicies(t, "", 0)
+ pol := loadPolicies(t, "testdata/zero_difficulty.yaml", 0)
ckieExpiration := 10 * time.Minute
srv := spawnAnubis(t, Options{
@@ -223,9 +268,7 @@ func TestCookieCustomExpiration(t *testing.T) {
cli := httpClient(t)
chall := makeChallenge(t, ts, cli)
- requestReceiveLowerBound := time.Now().Add(-1 * time.Minute)
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
- requestReceiveUpperBound := time.Now()
if resp.StatusCode != http.StatusFound {
resp.Write(os.Stderr)
@@ -244,19 +287,10 @@ func TestCookieCustomExpiration(t *testing.T) {
t.Errorf("Cookie %q not found", anubis.CookieName)
return
}
-
- expirationLowerBound := requestReceiveLowerBound.Add(ckieExpiration)
- expirationUpperBound := requestReceiveUpperBound.Add(ckieExpiration)
- // Since the cookie expiration precision is only to the second due to the Unix() call, we can
- // lower the level of expected precision.
- if ckie.Expires.Unix() < expirationLowerBound.Unix() || ckie.Expires.Unix() > expirationUpperBound.Unix() {
- t.Errorf("cookie expiration is not within the expected range. expected between: %v and %v. got: %v", expirationLowerBound, expirationUpperBound, ckie.Expires)
- return
- }
}
func TestCookieSettings(t *testing.T) {
- pol := loadPolicies(t, "", 0)
+ pol := loadPolicies(t, "testdata/zero_difficulty.yaml", 0)
srv := spawnAnubis(t, Options{
Next: http.NewServeMux(),
@@ -268,7 +302,6 @@ func TestCookieSettings(t *testing.T) {
CookieExpiration: anubis.CookieDefaultExpirationTime,
})
- requestReceiveLowerBound := time.Now()
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
defer ts.Close()
@@ -276,7 +309,6 @@ func TestCookieSettings(t *testing.T) {
chall := makeChallenge(t, ts, cli)
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
- requestReceiveUpperBound := time.Now()
if resp.StatusCode != http.StatusFound {
resp.Write(os.Stderr)
@@ -300,15 +332,6 @@ func TestCookieSettings(t *testing.T) {
t.Errorf("cookie domain is wrong, wanted 127.0.0.1, got: %s", ckie.Domain)
}
- expirationLowerBound := requestReceiveLowerBound.Add(anubis.CookieDefaultExpirationTime)
- expirationUpperBound := requestReceiveUpperBound.Add(anubis.CookieDefaultExpirationTime)
- // Since the cookie expiration precision is only to the second due to the Unix() call, we can
- // lower the level of expected precision.
- if ckie.Expires.Unix() < expirationLowerBound.Unix() || ckie.Expires.Unix() > expirationUpperBound.Unix() {
- t.Errorf("cookie expiration is not within the expected range. expected between: %v and %v. got: %v", expirationLowerBound, expirationUpperBound, ckie.Expires)
- return
- }
-
if ckie.Partitioned != srv.opts.CookiePartitioned {
t.Errorf("wanted partitioned flag %v, got: %v", srv.opts.CookiePartitioned, ckie.Partitioned)
}
@@ -325,7 +348,7 @@ func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
for i := 1; i < 10; i++ {
t.Run(fmt.Sprint(i), func(t *testing.T) {
- anubisPolicy := loadPolicies(t, "", i)
+ anubisPolicy := loadPolicies(t, "testdata/test_config_no_thresholds.yaml", i)
s, err := New(Options{
Next: h,
@@ -476,8 +499,11 @@ func TestBasePrefix(t *testing.T) {
q.Set("nonce", fmt.Sprint(nonce))
q.Set("redir", redir)
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
+ q.Set("id", chall.ID)
req.URL.RawQuery = q.Encode()
+ t.Log(req.URL.String())
+
resp, err = cli.Do(req)
if err != nil {
t.Fatalf("can't do challenge passing: %v", err)
diff --git a/lib/challenge/metarefresh/metarefresh.go b/lib/challenge/metarefresh/metarefresh.go
index db6fcc6..7655a9d 100644
--- a/lib/challenge/metarefresh/metarefresh.go
+++ b/lib/challenge/metarefresh/metarefresh.go
@@ -9,7 +9,6 @@ import (
"github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/localization"
- "github.com/TecharoHQ/anubis/web"
"github.com/a-h/templ"
)
@@ -32,16 +31,14 @@ 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.RandomData)
+ q.Set("id", in.Challenge.ID)
u.RawQuery = q.Encode()
loc := localization.GetLocalizer(r)
- 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)
- }
+ result := page(u.String(), in.Rule.Challenge.Difficulty, loc)
- return component, nil
+ return result, nil
}
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
diff --git a/lib/challenge/proofofwork/proofofwork.go b/lib/challenge/proofofwork/proofofwork.go
index a24a4f5..8cd3127 100644
--- a/lib/challenge/proofofwork/proofofwork.go
+++ b/lib/challenge/proofofwork/proofofwork.go
@@ -11,10 +11,11 @@ import (
"github.com/TecharoHQ/anubis/internal"
chall "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/localization"
- "github.com/TecharoHQ/anubis/web"
"github.com/a-h/templ"
)
+//go:generate go tool github.com/a-h/templ/cmd/templ generate
+
func init() {
chall.Register("fast", &Impl{Algorithm: "fast"})
chall.Register("slow", &Impl{Algorithm: "slow"})
@@ -24,18 +25,11 @@ type Impl struct {
Algorithm string
}
-func (i *Impl) Setup(mux *http.ServeMux) {
- /* no implementation required */
-}
+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.RandomData, in.Rule.Challenge, in.OGTags, loc)
- if err != nil {
- return nil, fmt.Errorf("can't render page: %w", err)
- }
-
- return component, nil
+ return page(loc), nil
}
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
diff --git a/lib/challenge/proofofwork/proofofwork.templ b/lib/challenge/proofofwork/proofofwork.templ
new file mode 100644
index 0000000..0187acc
--- /dev/null
+++ b/lib/challenge/proofofwork/proofofwork.templ
@@ -0,0 +1,39 @@
+package proofofwork
+
+import (
+ "github.com/TecharoHQ/anubis"
+ "github.com/TecharoHQ/anubis/lib/localization"
+)
+
+templ page(localizer *localization.SimpleLocalizer) {
+
+

+

+
{ localizer.T("loading") }
+
+
+
+ { localizer.T("why_am_i_seeing") }
+
+ { localizer.T("ai_companies_explanation") }
+
+
+ { localizer.T("anubis_compromise") }
+
+
+ { localizer.T("hack_purpose") }
+
+
+ { localizer.T("jshelter_note") }
+
+
+
+
+
+}
diff --git a/lib/challenge/proofofwork/proofofwork_templ.go b/lib/challenge/proofofwork/proofofwork_templ.go
new file mode 100644
index 0000000..b32bfae
--- /dev/null
+++ b/lib/challenge/proofofwork/proofofwork_templ.go
@@ -0,0 +1,175 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.924
+package proofofwork
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "github.com/TecharoHQ/anubis"
+ "github.com/TecharoHQ/anubis/lib/localization"
+)
+
+func page(localizer *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 {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
)
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 12, Col: 41}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("why_am_i_seeing"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 18, Col: 44}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 string
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("ai_companies_explanation"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 20, Col: 45}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("anubis_compromise"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 23, Col: 38}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("hack_purpose"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 26, Col: 33}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("jshelter_note"))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 29, Col: 34}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/lib/http.go b/lib/http.go
index 4f8ed02..d548f4d 100644
--- a/lib/http.go
+++ b/lib/http.go
@@ -174,13 +174,23 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
component, err := impl.Issue(r, lg, in)
if err != nil {
- 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.
+ lg.Error("[unexpected] challenge component 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, fmt.Sprintf("%s \"RenderIndex\"", localizer.T("internal_server_error")))
return
}
- handler := internal.GzipMiddleware(1, internal.NoStoreCache(templ.Handler(
+ page := web.BaseWithChallengeAndOGTags(
+ localizer.T("making_sure_not_bot"),
component,
+ s.policy.Impressum,
+ chall,
+ in.Rule.Challenge,
+ in.OGTags,
+ localizer,
+ )
+
+ handler := internal.GzipMiddleware(1, internal.NoStoreCache(templ.Handler(
+ page,
templ.WithStatus(s.opts.Policy.StatusCodes.Challenge),
)))
handler.ServeHTTP(w, r)
diff --git a/lib/policy/config/config.go b/lib/policy/config/config.go
index 20979e4..6b5946a 100644
--- a/lib/policy/config/config.go
+++ b/lib/policy/config/config.go
@@ -194,7 +194,7 @@ type ChallengeRules struct {
}
var (
- ErrChallengeDifficultyTooLow = errors.New("config.ChallengeRules: difficulty is too low (must be >= 1)")
+ ErrChallengeDifficultyTooLow = errors.New("config.ChallengeRules: difficulty is too low (must be >= 0)")
ErrChallengeDifficultyTooHigh = errors.New("config.ChallengeRules: difficulty is too high (must be <= 64)")
ErrChallengeMustHaveAlgorithm = errors.New("config.ChallengeRules: must have algorithm name set")
)
@@ -206,7 +206,7 @@ func (cr ChallengeRules) Valid() error {
errs = append(errs, ErrChallengeMustHaveAlgorithm)
}
- if cr.Difficulty < 1 {
+ if cr.Difficulty < 0 {
errs = append(errs, fmt.Errorf("%w, got: %d", ErrChallengeDifficultyTooLow, cr.Difficulty))
}
diff --git a/lib/policy/config/config_test.go b/lib/policy/config/config_test.go
index 40bb6b4..3a96c9c 100644
--- a/lib/policy/config/config_test.go
+++ b/lib/policy/config/config_test.go
@@ -109,7 +109,7 @@ func TestBotValid(t *testing.T) {
Action: RuleChallenge,
PathRegex: p("Mozilla"),
Challenge: &ChallengeRules{
- Difficulty: 0,
+ Difficulty: -1,
ReportAs: 4,
Algorithm: "fast",
},
diff --git a/lib/policy/config/threshold_test.go b/lib/policy/config/threshold_test.go
index 9024fe8..1c668b2 100644
--- a/lib/policy/config/threshold_test.go
+++ b/lib/policy/config/threshold_test.go
@@ -70,7 +70,7 @@ func TestThresholdValid(t *testing.T) {
name: "challenge invalid",
input: &Threshold{
Action: RuleChallenge,
- Challenge: &ChallengeRules{Difficulty: 0, ReportAs: 0},
+ Challenge: &ChallengeRules{Difficulty: -1, ReportAs: -1},
},
err: ErrChallengeDifficultyTooLow,
},
diff --git a/lib/testdata/test_config.yaml b/lib/testdata/test_config.yaml
index 11f7cb4..9047dcb 100644
--- a/lib/testdata/test_config.yaml
+++ b/lib/testdata/test_config.yaml
@@ -35,4 +35,11 @@ status_codes:
CHALLENGE: 200
DENY: 200
-thresholds: []
+thresholds:
+ - name: minimal-suspicion
+ expression: "true"
+ action: CHALLENGE
+ challenge:
+ algorithm: fast
+ difficulty: 1
+ report_as: 1
diff --git a/lib/testdata/test_config_no_thresholds.yaml b/lib/testdata/test_config_no_thresholds.yaml
new file mode 100644
index 0000000..11f7cb4
--- /dev/null
+++ b/lib/testdata/test_config_no_thresholds.yaml
@@ -0,0 +1,38 @@
+bots:
+ - import: (data)/bots/_deny-pathological.yaml
+ - import: (data)/bots/aggressive-brazilian-scrapers.yaml
+ - import: (data)/meta/ai-block-aggressive.yaml
+ - import: (data)/crawlers/_allow-good.yaml
+ - import: (data)/clients/x-firefox-ai.yaml
+ - import: (data)/common/keep-internet-working.yaml
+ - name: countries-with-aggressive-scrapers
+ action: WEIGH
+ geoip:
+ countries:
+ - BR
+ - CN
+ weight:
+ adjust: 10
+ - name: aggressive-asns-without-functional-abuse-contact
+ action: WEIGH
+ asns:
+ match:
+ - 13335 # Cloudflare
+ - 136907 # Huawei Cloud
+ - 45102 # Alibaba Cloud
+ weight:
+ adjust: 10
+ - name: generic-browser
+ user_agent_regex: >-
+ Mozilla|Opera
+ action: WEIGH
+ weight:
+ adjust: 10
+
+dnsbl: false
+
+status_codes:
+ CHALLENGE: 200
+ DENY: 200
+
+thresholds: []
diff --git a/lib/testdata/zero_difficulty.yaml b/lib/testdata/zero_difficulty.yaml
new file mode 100644
index 0000000..75382db
--- /dev/null
+++ b/lib/testdata/zero_difficulty.yaml
@@ -0,0 +1,45 @@
+bots:
+ - import: (data)/bots/_deny-pathological.yaml
+ - import: (data)/bots/aggressive-brazilian-scrapers.yaml
+ - import: (data)/meta/ai-block-aggressive.yaml
+ - import: (data)/crawlers/_allow-good.yaml
+ - import: (data)/clients/x-firefox-ai.yaml
+ - import: (data)/common/keep-internet-working.yaml
+ - name: countries-with-aggressive-scrapers
+ action: WEIGH
+ geoip:
+ countries:
+ - BR
+ - CN
+ weight:
+ adjust: 10
+ - name: aggressive-asns-without-functional-abuse-contact
+ action: WEIGH
+ asns:
+ match:
+ - 13335 # Cloudflare
+ - 136907 # Huawei Cloud
+ - 45102 # Alibaba Cloud
+ weight:
+ adjust: 10
+ - name: generic-browser
+ user_agent_regex: >-
+ Mozilla|Opera
+ action: WEIGH
+ weight:
+ adjust: 10
+
+dnsbl: false
+
+status_codes:
+ CHALLENGE: 200
+ DENY: 200
+
+thresholds:
+ - name: minimal-suspicion
+ expression: "true"
+ action: CHALLENGE
+ challenge:
+ algorithm: fast
+ difficulty: 0
+ report_as: 0
diff --git a/web/index.go b/web/index.go
index ee2042b..c3ba03d 100644
--- a/web/index.go
+++ b/web/index.go
@@ -3,6 +3,7 @@ package web
import (
"github.com/a-h/templ"
+ "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/localization"
"github.com/TecharoHQ/anubis/lib/policy/config"
)
@@ -11,18 +12,14 @@ func Base(title string, body templ.Component, impressum *config.Impressum, local
return base(title, body, impressum, nil, nil, localizer)
}
-func BaseWithChallengeAndOGTags(title string, body templ.Component, impressum *config.Impressum, challenge string, rules *config.ChallengeRules, ogTags map[string]string, localizer *localization.SimpleLocalizer) (templ.Component, error) {
+func BaseWithChallengeAndOGTags(title string, body templ.Component, impressum *config.Impressum, challenge *challenge.Challenge, rules *config.ChallengeRules, ogTags map[string]string, localizer *localization.SimpleLocalizer) templ.Component {
return base(title, body, impressum, struct {
Rules *config.ChallengeRules `json:"rules"`
- Challenge string `json:"challenge"`
+ Challenge any `json:"challenge"`
}{
Challenge: challenge,
Rules: rules,
- }, ogTags, localizer), nil
-}
-
-func Index(localizer *localization.SimpleLocalizer) templ.Component {
- return index(localizer)
+ }, ogTags, localizer)
}
func ErrorPage(msg, mail string, localizer *localization.SimpleLocalizer) templ.Component {
diff --git a/web/index.templ b/web/index.templ
index a018669..65d704c 100644
--- a/web/index.templ
+++ b/web/index.templ
@@ -58,9 +58,7 @@ templ base(title string, body templ.Component, impressum *config.Impressum, chal
}
@templ.JSONScript("anubis_version", anubis.Version)
- if challenge != nil {
- @templ.JSONScript("anubis_challenge", challenge)
- }
+ @templ.JSONScript("anubis_challenge", challenge)
@templ.JSONScript("anubis_base_prefix", anubis.BasePrefix)
@@ -81,6 +79,7 @@ templ base(title string, body templ.Component, impressum *config.Impressum, chal
-- Imprint
}
+ { localizer.T("version_info") } { anubis.Version }.
@@ -88,40 +87,6 @@ templ base(title string, body templ.Component, impressum *config.Impressum, chal
}
-templ index(localizer *localization.SimpleLocalizer) {
-
-

-

-
{ localizer.T("loading") }
-
-
-
- { localizer.T("why_am_i_seeing") }
-
- { localizer.T("ai_companies_explanation") }
-
-
- { localizer.T("anubis_compromise") }
-
-
- { localizer.T("hack_purpose") }
-
-
- { localizer.T("jshelter_note") }
-
- { localizer.T("version_info") } { anubis.Version }.
-
-
-
-
-}
-
templ errorPage(message, mail string, localizer *localization.SimpleLocalizer) {

diff --git a/web/index_templ.go b/web/index_templ.go
index d502689..ee72ec1 100644
--- a/web/index_templ.go
+++ b/web/index_templ.go
@@ -120,11 +120,9 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- if challenge != nil {
- templ_7745c5c3_Err = templ.JSONScript("anubis_challenge", challenge).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
+ templ_7745c5c3_Err = templ.JSONScript("anubis_challenge", challenge).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.JSONScript("anubis_base_prefix", anubis.BasePrefix).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
@@ -137,7 +135,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 68, Col: 47}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 66, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -158,7 +156,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("protected_by"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 73, Col: 36}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 71, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -171,7 +169,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("protected_from"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 73, Col: 127}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 71, Col: 127}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -184,7 +182,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("made_with"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 75, Col: 40}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 73, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -197,7 +195,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("mascot_design"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 77, Col: 39}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 75, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -210,7 +208,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("celphase"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 77, Col: 123}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 75, Col: 123}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -236,7 +234,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("%simprint", anubis.APIPrefix)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 81, Col: 78}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 79, Col: 78}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -247,192 +245,33 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "