Compare commits

...

4 Commits

Author SHA1 Message Date
Jason Cameron
978af9d5ff chore: add comments back to Challenge struct.
See #1284
and https://github.com/TecharoHQ/anubis/pull/1284#issuecomment-3784096905
2026-01-22 09:46:38 -05:00
Timon de Groot
57c0b2b22c Add IP mapped Perplexity user agents (#1393)
Perplexity has some proper documentation available for their crawlers,
with published IP addresses: https://docs.perplexity.ai/guides/bots.

Signed-off-by: Timon de Groot <timon.degroot@team.blue>
2026-01-15 19:57:31 -05:00
Thomas Arrow
186ffeb744 docs: clarify botstopper kubernetes instructions (#1404)
This makes it clear that when generating a kubernetes secret to pull the bot stopper image that:
- no email is required
- a user is required but the actual value of the username is not checked
- the GH token needs to be pasted in

Signed-off-by: Thomas Arrow <tarrow@users.noreply.github.com>
2026-01-15 11:13:10 +00:00
Xe Iaso
ff87aac4e7 fix(web): include base prefix in generated URLs (#1403)
* fix(web): include base prefix in generated URLs

Forgot to add the base prefix to these URLs. Committed a fix for this
and added a test to ensure this does not repeat. Oops!

Closes: #1402
Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: update CHANGELOG

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-01-14 23:47:44 +00:00
13 changed files with 134 additions and 20 deletions

View File

@@ -23,3 +23,4 @@ fout
iplist
NArg
blocklists
rififi

View File

@@ -4,5 +4,5 @@
# - Claude-User: No published IP allowlist
- name: "ai-clients"
user_agent_regex: >-
ChatGPT-User|Claude-User|MistralAI-User
ChatGPT-User|Claude-User|MistralAI-User|Perplexity-User
action: DENY

View File

@@ -0,0 +1,12 @@
# Acts on behalf of user requests
# https://docs.perplexity.ai/guides/bots
- name: perplexity-user
user_agent_regex: Perplexity-User/.+; \+https\://perplexity\.ai/perplexity-user
action: ALLOW
# https://www.perplexity.com/perplexity-user.json
remote_addresses: [
"44.208.221.197/32",
"34.193.163.52/32",
"18.97.21.0/30",
"18.97.43.80/29",
]

View File

@@ -4,5 +4,5 @@
# - Claude-SearchBot: No published IP allowlist
- name: "ai-crawlers-search"
user_agent_regex: >-
OAI-SearchBot|Claude-SearchBot
OAI-SearchBot|Claude-SearchBot|PerplexityBot
action: DENY

View File

@@ -0,0 +1,16 @@
# Indexing for search, does not collect training data
# https://docs.perplexity.ai/guides/bots
- name: perplexitybot
user_agent_regex: PerplexityBot/.+; \+https\://perplexity\.ai/perplexitybot
action: ALLOW
# https://www.perplexity.com/perplexitybot.json
remote_addresses: [
"107.20.236.150/32",
"3.224.62.45/32",
"18.210.92.235/32",
"3.222.232.239/32",
"3.211.124.183/32",
"3.231.139.107/32",
"18.97.1.228/30",
"18.97.9.96/29",
]

View File

@@ -3,5 +3,7 @@
- import: (data)/bots/ai-catchall.yaml
- import: (data)/crawlers/ai-training.yaml
- import: (data)/crawlers/openai-searchbot.yaml
- import: (data)/crawlers/perplexitybot.yaml
- import: (data)/clients/openai-chatgpt-user.yaml
- import: (data)/clients/mistral-mistralai-user.yaml
- import: (data)/clients/mistral-mistralai-user.yaml
- import: (data)/clients/perplexity-user.yaml

View File

@@ -2,5 +2,7 @@
- import: (data)/bots/ai-catchall.yaml
- import: (data)/crawlers/openai-searchbot.yaml
- import: (data)/crawlers/openai-gptbot.yaml
- import: (data)/crawlers/perplexitybot.yaml
- import: (data)/clients/openai-chatgpt-user.yaml
- import: (data)/clients/mistral-mistralai-user.yaml
- import: (data)/clients/mistral-mistralai-user.yaml
- import: (data)/clients/perplexity-user.yaml

View File

@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add iplist2rule tool that lets admins turn an IP address blocklist into an Anubis ruleset.
- Add Polish locale ([#1292](https://github.com/TecharoHQ/anubis/pull/1309))
- Fix honeypot and imprint links missing `BASE_PREFIX` when deployed behind a path prefix ([#1402](https://github.com/TecharoHQ/anubis/issues/1402))
<!-- This changes the project to: -->

View File

@@ -51,9 +51,8 @@ If you are using Kubernetes, you will need to create an image pull secret:
kubectl create secret docker-registry \
techarohq-botstopper \
--docker-server ghcr.io \
--docker-username your-username \
--docker-password your-access-token \
--docker-email your@email.address
--docker-username any-username \
--docker-password <your-access-token> \
```
Then attach it to your Deployment:

View File

@@ -4,12 +4,12 @@ import "time"
// Challenge is the metadata about a single challenge issuance.
type Challenge struct {
IssuedAt time.Time `json:"issuedAt"`
Metadata map[string]string `json:"metadata"`
ID string `json:"id"`
Method string `json:"method"`
RandomData string `json:"randomData"`
PolicyRuleHash string `json:"policyRuleHash,omitempty"`
Difficulty int `json:"difficulty,omitempty"`
Spent bool `json:"spent"`
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
ID string `json:"id"` // UUID identifying the challenge
Method string `json:"method"` // Challenge method
RandomData string `json:"randomData"` // The random data the client processes
PolicyRuleHash string `json:"policyRuleHash,omitempty"` // Hash of the policy rule that issued this challenge
Difficulty int `json:"difficulty,omitempty"` // Difficulty that was in effect when issued
Spent bool `json:"spent"` // Has the challenge already been solved?
}

View File

@@ -64,7 +64,7 @@ templ base(title string, body templ.Component, impressum *config.Impressum, chal
@templ.JSONScript("anubis_public_url", anubis.PublicUrl)
</head>
<body id="top">
@honeypotLink(fmt.Sprintf("%shoneypot/%s/init", anubis.APIPrefix, uuid.NewString()))
@honeypotLink(anubis.BasePrefix + fmt.Sprintf("%shoneypot/%s/init", anubis.APIPrefix, uuid.NewString()))
<main>
<h1 id="title" class="centered-div">{ title }</h1>
@body
@@ -79,7 +79,7 @@ templ base(title string, body templ.Component, impressum *config.Impressum, chal
if impressum != nil {
<p>
@templ.Raw(impressum.Footer)
-- <a href={ templ.SafeURL(fmt.Sprintf("%simprint", anubis.APIPrefix)) }>Imprint</a>
-- <a href={ templ.SafeURL(anubis.BasePrefix + fmt.Sprintf("%simprint", anubis.APIPrefix)) }>Imprint</a>
</p>
}
<p>{ localizer.T("version_info") } <code>{ anubis.Version }</code>.</p>

6
web/index_templ.go generated
View File

@@ -137,7 +137,7 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = honeypotLink(fmt.Sprintf("%shoneypot/%s/init", anubis.APIPrefix, uuid.NewString())).Render(ctx, templ_7745c5c3_Buffer)
templ_7745c5c3_Err = honeypotLink(anubis.BasePrefix+fmt.Sprintf("%shoneypot/%s/init", anubis.APIPrefix, uuid.NewString())).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -245,9 +245,9 @@ func base(title string, body templ.Component, impressum *config.Impressum, chall
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("%simprint", anubis.APIPrefix)))
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(anubis.BasePrefix + fmt.Sprintf("%simprint", anubis.APIPrefix)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 82, Col: 78}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 82, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {

81
web/index_test.go Normal file
View File

@@ -0,0 +1,81 @@
package web
import (
"context"
"net/http/httptest"
"strings"
"testing"
"github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/localization"
"github.com/a-h/templ"
)
func TestBasePrefixInLinks(t *testing.T) {
tests := []struct {
name string
basePrefix string
wantInLink string
}{
{
name: "no prefix",
basePrefix: "",
wantInLink: "/.within.website/x/cmd/anubis/api/",
},
{
name: "with rififi prefix",
basePrefix: "/rififi",
wantInLink: "/rififi/.within.website/x/cmd/anubis/api/",
},
{
name: "with myapp prefix",
basePrefix: "/myapp",
wantInLink: "/myapp/.within.website/x/cmd/anubis/api/",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Save original BasePrefix and restore after test
origPrefix := anubis.BasePrefix
defer func() { anubis.BasePrefix = origPrefix }()
anubis.BasePrefix = tt.basePrefix
// Create test impressum
impressum := &config.Impressum{
Footer: "<p>Test footer</p>",
Page: config.ImpressumPage{
Title: "Test Imprint",
Body: "<p>Test imprint body</p>",
},
}
// Create localizer using a dummy request
req := httptest.NewRequest("GET", "/", nil)
localizer := &localization.SimpleLocalizer{}
localizer.Localizer = localization.NewLocalizationService().GetLocalizerFromRequest(req)
// Render the base template to a buffer
var buf strings.Builder
component := base(tt.name, templ.NopComponent, impressum, nil, nil, localizer)
err := component.Render(context.Background(), &buf)
if err != nil {
t.Fatalf("failed to render template: %v", err)
}
output := buf.String()
// Check that honeypot link includes the base prefix
if !strings.Contains(output, `href="`+tt.wantInLink+`honeypot/`) {
t.Errorf("honeypot link does not contain base prefix %q\noutput: %s", tt.wantInLink, output)
}
// Check that imprint link includes the base prefix
if !strings.Contains(output, `href="`+tt.wantInLink+`imprint`) {
t.Errorf("imprint link does not contain base prefix %q\noutput: %s", tt.wantInLink, output)
}
})
}
}