Compare commits

..

9 Commits

Author SHA1 Message Date
Xe Iaso 9b3cce4d4f chore: spelling
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-22 19:43:44 -04:00
Xe Iaso 56ec19d2da doc: document metrics TLS and mTLS
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-22 19:41:18 -04:00
Xe Iaso 11ab78ec33 doc(default-config): document how to set up TLS and mTLS
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-22 19:41:01 -04:00
Xe Iaso 11f944128f feat(metrics): enable mTLS support
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-22 19:40:21 -04:00
Xe Iaso dfeb02b4ae feat(config): add CA certificate config value
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-22 19:32:11 -04:00
Xe Iaso b66630df74 fix(metrics): properly surface errors with the metrics server
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-22 19:15:22 -04:00
Xe Iaso 63e6a15280 feat(metrics): import keypairreloader from a private project
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-22 13:52:40 -04:00
Xe Iaso 888c477933 feat(metrics): add naive TLS serving for metrics
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-22 10:38:46 -04:00
Xe Iaso cda06f8c71 feat(config): add metrics TLS configuration
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-04-22 10:36:32 -04:00
52 changed files with 786 additions and 1893 deletions
-3
View File
@@ -38,6 +38,3 @@ Samsung
wenet wenet
qwertiko qwertiko
setuplistener setuplistener
mba
xfu
xou
-3
View File
@@ -120,7 +120,6 @@ fahedouch
fastcgi fastcgi
FCr FCr
fcrdns fcrdns
fcvg
fediverse fediverse
ffprobe ffprobe
fhdr fhdr
@@ -239,7 +238,6 @@ mnt
Mojeek Mojeek
mojeekbot mojeekbot
mozilla mozilla
mqvh
myclient myclient
mymaster mymaster
mypass mypass
@@ -389,7 +387,6 @@ vnd
VPS VPS
Vultr Vultr
WAIFU WAIFU
wcg
weblate weblate
webmaster webmaster
webpage webpage
-1
View File
@@ -27,7 +27,6 @@ jobs:
- palemoon/amd64 - palemoon/amd64
#- palemoon/i386 #- palemoon/i386
- robots_txt - robots_txt
- traefik
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
+1 -1
View File
@@ -259,7 +259,7 @@ func main() {
} }
lg.Info("loading policy file", "fname", *policyFname) lg.Info("loading policy file", "fname", *policyFname)
policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty, *slogLevel, strings.TrimSpace(*target) == "") policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty, *slogLevel)
if err != nil { if err != nil {
log.Fatalf("can't parse policy file: %v", err) log.Fatalf("can't parse policy file: %v", err)
} }
-7
View File
@@ -175,13 +175,6 @@ status_codes:
# bind: ":9090" # bind: ":9090"
# network: "tcp" # network: "tcp"
# #
# # To protect your metrics server with basic auth, set credentials below:
# #
# # https://anubis.techaro.lol/docs/admin/policies#http-basic-authentication
# basicAuth:
# username: ""
# password: ""
#
# # To serve metrics over TLS, set the path to the right TLS certificate and key # # To serve metrics over TLS, set the path to the right TLS certificate and key
# # here. When the files change on disk, they will automatically be reloaded. # # here. When the files change on disk, they will automatically be reloaded.
# # # #
+4 -13
View File
@@ -13,24 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- This changes the project to: --> <!-- This changes the project to: -->
- Patch [GHSA-6wcg-mqvh-fcvg](https://github.com/TecharoHQ/anubis/security/advisories/GHSA-6wcg-mqvh-fcvg) by containing subrequest logic to Anubis instances in subrequest mode - Move metrics server configuration to [the policy file](./admin/policies.mdx#metrics-server).
- Implement robot9001 style delays on the honeypot feature so that the first hit takes 1 millisecond, the second takes 2, etc
- Move metrics server configuration to [the policy file](./admin/policies.mdx#metrics-server)
- Expose [pprof endpoints](https://pkg.go.dev/net/http/pprof) on the metrics listener to enable profiling Anubis in production. - Expose [pprof endpoints](https://pkg.go.dev/net/http/pprof) on the metrics listener to enable profiling Anubis in production.
- fix: prevent nil pointer panic in challenge validation when threshold rules match during PassChallenge (#1463) - fix: prevent nil pointer panic in challenge validation when threshold rules match during PassChallenge (#1463)
- Instruct reverse proxies to not cache error pages - Instruct reverse proxies to not cache error pages.
- Fixed mixed tab/space indentation in Caddy documentation code block - Fixed mixed tab/space indentation in Caddy documentation code block
- Improve error messages and fix broken REDIRECT_DOMAINS link in docs ([#1193](https://github.com/TecharoHQ/anubis/issues/1193)) - Improve error messages and fix broken REDIRECT_DOMAINS link in docs ([#1193](https://github.com/TecharoHQ/anubis/issues/1193))
- Add Bulgarian locale ([#1394](https://github.com/TecharoHQ/anubis/pull/1394)) - Add Bulgarian locale ([#1394](https://github.com/TecharoHQ/anubis/pull/1394))
- Fixed case-sensitivity mismatch in geoipchecker.go - Fix CEL internal errors when iterating `headers`/`query` map wrappers by implementing map iterators for `HTTPHeaders` and `URLValues` ([#1465](https://github.com/TecharoHQ/anubis/pull/1465)).
- Fix CEL internal errors when iterating `headers`/`query` map wrappers by implementing map iterators for `HTTPHeaders` and `URLValues` ([#1465](https://github.com/TecharoHQ/anubis/pull/1465)) - Enable [metrics serving via TLS](./admin/policies.mdx#tls), including [mutual TLS (mTLS)](./admin/policies.mdx#mtls).
- Enable [metrics serving via TLS](./admin/policies.mdx#tls), including [mutual TLS (mTLS)](./admin/policies.mdx#mtls)
- Enable [HTTP basic auth](./admin/policies.mdx#http-basic-authentication) for the metrics server
- Fix a bug in the dataset poisoning maze that could allow denial of service [#1580](https://github.com/TecharoHQ/anubis/issues/1580)
- Add config option to add ASN to logs/metrics
- Log weight when issuing challenge
- Keep Anubis server URL state local to each `lib.Server` instance to make embedded use safer
- Fix `path_regex` and CEL `path` rules not matching when using Traefik `forwardAuth` middleware. Anubis now checks `X-Forwarded-Uri` (Traefik) in addition to `X-Original-URI` (nginx) when resolving the request path in subrequest mode ([#1628](https://github.com/TecharoHQ/anubis/issues/1628))
## v1.25.0: Necron ## v1.25.0: Necron
-19
View File
@@ -171,24 +171,6 @@ metrics:
As it is not expected for certificate authority certificates to change often, the CA certificate will NOT be automatically reloaded when the respective file changes. As it is not expected for certificate authority certificates to change often, the CA certificate will NOT be automatically reloaded when the respective file changes.
### HTTP basic authentication
Anubis' metrics server also supports setting HTTP basic auth as a lightweight protection against unauthorized users viewing metrics data. As the basic auth credentials are hardcoded in the configuration file, administrators SHOULD use randomly generated credentials, such as type-4 UUIDs or other high entropy strings. These credentials MUST NOT be sensitive or used to protect other high value systems.
Configure it with the `basicAuth` block under `metrics`:
```yaml
metrics:
bind: ":9090"
network: "tcp"
basicAuth:
username: azurediamond
password: hunter2
```
If you have Python installed, you can generate a high entropy password with `python -c 'import secrets; print(secrets.token_urlsafe(32))'`.
## Imprint / Impressum support ## Imprint / Impressum support
Anubis has support for showing imprint / impressum information. This is defined in the `impressum` block of your configuration. See [Imprint / Impressum configuration](./configuration/impressum.mdx) for more information. Anubis has support for showing imprint / impressum information. This is defined in the `impressum` block of your configuration. See [Imprint / Impressum configuration](./configuration/impressum.mdx) for more information.
@@ -411,7 +393,6 @@ Anubis exposes the following logging settings in the policy file:
| `level` | [log level](#log-levels) | `info` | The logging level threshold. Any logs that are at or above this threshold will be drained to the sink. Any other logs will be discarded. | | `level` | [log level](#log-levels) | `info` | The logging level threshold. Any logs that are at or above this threshold will be drained to the sink. Any other logs will be discarded. |
| `sink` | string | `stdio`, `file` | The sink where the logs drain to as they are being recorded in Anubis. | | `sink` | string | `stdio`, `file` | The sink where the logs drain to as they are being recorded in Anubis. |
| `parameters` | object | | Parameters for the given logging sink. This will vary based on the logging sink of choice. See below for more information. | | `parameters` | object | | Parameters for the given logging sink. This will vary based on the logging sink of choice. See below for more information. |
| `asn` | bool | `true`, `false` | Add ASN information to logs/metrics. (Requires a Thoth client configured) |
Anubis supports the following logging sinks: Anubis supports the following logging sinks:
+1 -1
View File
@@ -106,7 +106,7 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.16.2 // indirect github.com/go-git/go-git/v5 v5.16.2 // indirect
github.com/go-jose/go-jose/v3 v3.0.5 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
+2 -2
View File
@@ -189,8 +189,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-52
View File
@@ -1,52 +0,0 @@
package internal
import (
"crypto/sha256"
"crypto/subtle"
"fmt"
"log/slog"
"net/http"
)
// BasicAuth wraps next in HTTP Basic authentication using the provided
// credentials. If either username or password is empty, next is returned
// unchanged and a debug log line is emitted.
//
// Credentials are compared in constant time to avoid leaking information
// through timing side channels.
func BasicAuth(realm, username, password string, next http.Handler) http.Handler {
if username == "" || password == "" {
slog.Debug("skipping middleware, basic auth credentials are empty")
return next
}
expectedUser := sha256.Sum256([]byte(username))
expectedPass := sha256.Sum256([]byte(password))
challenge := fmt.Sprintf("Basic realm=%q, charset=\"UTF-8\"", realm)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok {
unauthorized(w, challenge)
return
}
gotUser := sha256.Sum256([]byte(user))
gotPass := sha256.Sum256([]byte(pass))
userMatch := subtle.ConstantTimeCompare(gotUser[:], expectedUser[:])
passMatch := subtle.ConstantTimeCompare(gotPass[:], expectedPass[:])
if userMatch&passMatch != 1 {
unauthorized(w, challenge)
return
}
next.ServeHTTP(w, r)
})
}
func unauthorized(w http.ResponseWriter, challenge string) {
w.Header().Set("WWW-Authenticate", challenge)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
-138
View File
@@ -1,138 +0,0 @@
package internal
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func okHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
}
func TestBasicAuth(t *testing.T) {
t.Parallel()
const (
realm = "test-realm"
username = "admin"
password = "hunter2"
)
for _, tt := range []struct {
name string
setAuth bool
user string
pass string
wantStatus int
wantBody string
wantChall bool
}{
{
name: "valid credentials",
setAuth: true,
user: username,
pass: password,
wantStatus: http.StatusOK,
wantBody: "ok",
},
{
name: "missing credentials",
setAuth: false,
wantStatus: http.StatusUnauthorized,
wantChall: true,
},
{
name: "wrong username",
setAuth: true,
user: "nobody",
pass: password,
wantStatus: http.StatusUnauthorized,
wantChall: true,
},
{
name: "wrong password",
setAuth: true,
user: username,
pass: "wrong",
wantStatus: http.StatusUnauthorized,
wantChall: true,
},
{
name: "empty supplied credentials",
setAuth: true,
user: "",
pass: "",
wantStatus: http.StatusUnauthorized,
wantChall: true,
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
h := BasicAuth(realm, username, password, okHandler())
req := httptest.NewRequest(http.MethodGet, "/", nil)
if tt.setAuth {
req.SetBasicAuth(tt.user, tt.pass)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != tt.wantStatus {
t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
}
if tt.wantBody != "" && rec.Body.String() != tt.wantBody {
t.Errorf("body = %q, want %q", rec.Body.String(), tt.wantBody)
}
chall := rec.Header().Get("WWW-Authenticate")
if tt.wantChall {
if chall == "" {
t.Error("WWW-Authenticate header missing on 401")
}
if !strings.Contains(chall, realm) {
t.Errorf("WWW-Authenticate = %q, want realm %q", chall, realm)
}
} else if chall != "" {
t.Errorf("unexpected WWW-Authenticate header: %q", chall)
}
})
}
}
func TestBasicAuthPassthrough(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
name string
username string
password string
}{
{name: "empty username", username: "", password: "hunter2"},
{name: "empty password", username: "admin", password: ""},
{name: "both empty", username: "", password: ""},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
h := BasicAuth("realm", tt.username, tt.password, okHandler())
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d (passthrough expected)", rec.Code, http.StatusOK)
}
if rec.Body.String() != "ok" {
t.Errorf("body = %q, want %q", rec.Body.String(), "ok")
}
})
}
}
+22 -17
View File
@@ -5,7 +5,6 @@ import (
_ "embed" _ "embed"
"fmt" "fmt"
"log/slog" "log/slog"
"math"
"math/rand/v2" "math/rand/v2"
"net/http" "net/http"
"net/netip" "net/netip"
@@ -77,6 +76,13 @@ type Impl struct {
affirmation, body, title spintax.Spintax affirmation, body, title spintax.Spintax
} }
func (i *Impl) incrementUA(ctx context.Context, userAgent string) int {
result, _ := i.uaWeight.Get(ctx, internal.SHA256sum(userAgent))
result++
i.uaWeight.Set(ctx, internal.SHA256sum(userAgent), result, time.Hour)
return result
}
func (i *Impl) incrementNetwork(ctx context.Context, network string) int { func (i *Impl) incrementNetwork(ctx context.Context, network string) int {
result, _ := i.networkWeight.Get(ctx, internal.SHA256sum(network)) result, _ := i.networkWeight.Get(ctx, internal.SHA256sum(network))
result++ result++
@@ -84,19 +90,20 @@ func (i *Impl) incrementNetwork(ctx context.Context, network string) int {
return result return result
} }
func (i *Impl) CheckUA() checker.Impl {
return checker.Func(func(r *http.Request) (bool, error) {
result, _ := i.uaWeight.Get(r.Context(), internal.SHA256sum(r.UserAgent()))
if result >= 25 {
return true, nil
}
return false, nil
})
}
func (i *Impl) CheckNetwork() checker.Impl { func (i *Impl) CheckNetwork() checker.Impl {
return checker.Func(func(r *http.Request) (bool, error) { return checker.Func(func(r *http.Request) (bool, error) {
realIP, _ := internal.RealIP(r) result, _ := i.uaWeight.Get(r.Context(), internal.SHA256sum(r.UserAgent()))
if !realIP.IsValid() {
realIP = netip.MustParseAddr(r.Header.Get("X-Real-Ip"))
}
network, ok := internal.ClampIP(realIP)
if !ok {
return false, nil
}
result, _ := i.networkWeight.Get(r.Context(), internal.SHA256sum(network.String()))
if result >= 25 { if result >= 25 {
return true, nil return true, nil
} }
@@ -157,6 +164,7 @@ func (i *Impl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
networkCount := i.incrementNetwork(r.Context(), network.String()) networkCount := i.incrementNetwork(r.Context(), network.String())
uaCount := i.incrementUA(r.Context(), r.UserAgent())
stage := r.PathValue("stage") stage := r.PathValue("stage")
@@ -164,14 +172,11 @@ func (i *Impl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
lg.Debug("found new entrance point", "id", id, "stage", stage, "userAgent", r.UserAgent(), "clampedIP", network) lg.Debug("found new entrance point", "id", id, "stage", stage, "userAgent", r.UserAgent(), "clampedIP", network)
} else { } else {
switch { switch {
case networkCount%256 == 0: case networkCount%256 == 0, uaCount%256 == 0:
lg.Warn("found possible crawler", "id", id, "network", network, "userAgent", r.UserAgent()) lg.Warn("found possible crawler", "id", id, "network", network)
} }
} }
millisecondAmount := math.Pow(float64(networkCount), 2)
time.Sleep(time.Duration(millisecondAmount) * time.Millisecond)
spins := i.makeSpins() spins := i.makeSpins()
affirmations := i.makeAffirmations() affirmations := i.makeAffirmations()
title := i.makeTitle() title := i.makeTitle()
+1 -1
View File
@@ -595,7 +595,7 @@ func spawnAnubisWithOptions(t *testing.T, basePrefix string) string {
fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix()) fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix())
}) })
policy, err := libanubis.LoadPoliciesOrDefault(t.Context(), "", anubis.DefaultDifficulty, "info", false) policy, err := libanubis.LoadPoliciesOrDefault(t.Context(), "", anubis.DefaultDifficulty, "info")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+30 -114
View File
@@ -11,7 +11,6 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time" "time"
@@ -33,8 +32,6 @@ import (
"github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/policy/checker" "github.com/TecharoHQ/anubis/lib/policy/checker"
"github.com/TecharoHQ/anubis/lib/store" "github.com/TecharoHQ/anubis/lib/store"
"github.com/TecharoHQ/anubis/web"
iptoasnv1 "github.com/TecharoHQ/thoth-proto/gen/techaro/thoth/iptoasn/v1"
// challenge implementations // challenge implementations
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh" _ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
@@ -42,52 +39,31 @@ import (
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork" _ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
) )
type contextKey int
const asnContextKey contextKey = iota
type asnInfo struct {
ASN string
Description string
}
func asnFromContext(ctx context.Context) (string, string) {
if v, ok := ctx.Value(asnContextKey).(asnInfo); ok {
return v.ASN, v.Description
}
return "", ""
}
var ( var (
challengesIssued = promauto.NewCounterVec(prometheus.CounterOpts{ challengesIssued = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "anubis_challenges_issued", Name: "anubis_challenges_issued",
Help: "The total number of challenges issued", Help: "The total number of challenges issued",
}, []string{"method", "asn", "asn_description"}) }, []string{"method"})
challengesValidated = promauto.NewCounterVec(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", "asn", "asn_description"}) }, []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", "asn", "asn_description"}) }, []string{"status"})
failedValidations = promauto.NewCounterVec(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", "asn", "asn_description"}) }, []string{"method"})
requestsProxied = promauto.NewCounterVec(prometheus.CounterOpts{ requestsProxied = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "anubis_proxied_requests_total", Name: "anubis_proxied_requests_total",
Help: "Number of requests proxied through Anubis to upstream targets", Help: "Number of requests proxied through Anubis to upstream targets",
}, []string{"host", "asn", "asn_description"}) }, []string{"host"})
requestsByASN = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "anubis_requests_by_asn_total",
Help: "Number of requests by ASN",
}, []string{"asn", "asn_description"})
) )
type Server struct { type Server struct {
@@ -98,70 +74,10 @@ type Server struct {
OGTags *ogtags.OGTagCache OGTags *ogtags.OGTagCache
logger *slog.Logger logger *slog.Logger
opts Options opts Options
basePrefix string
publicURL string
ed25519Priv ed25519.PrivateKey ed25519Priv ed25519.PrivateKey
hs512Secret []byte hs512Secret []byte
} }
func (s *Server) getRequestLogger(r *http.Request) (*slog.Logger, *http.Request) {
lg := internal.GetRequestLogger(s.logger, r)
if s.policy.LogASN && s.policy.ThothClient != nil {
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
defer cancel()
ip := r.Header.Get("X-Real-Ip")
if info, err := s.policy.ThothClient.IPToASN.Lookup(ctx, &iptoasnv1.LookupRequest{IpAddress: ip}); err == nil && info.GetAnnounced() {
asn := strconv.FormatUint(uint64(info.GetAsNumber()), 10)
lg = lg.With("asn", info.GetAsNumber(), "asn_description", info.GetDescription())
requestsByASN.WithLabelValues(asn, info.GetDescription()).Inc()
r = r.WithContext(context.WithValue(r.Context(), asnContextKey, asnInfo{
ASN: asn,
Description: info.GetDescription(),
}))
}
}
return lg, r
}
func (s *Server) configuredBasePrefix() string {
if s.basePrefix != "" {
return s.basePrefix
}
return strings.TrimRight(s.opts.BasePrefix, "/")
}
func (s *Server) configuredPublicURL() string {
if s.publicURL != "" {
return s.publicURL
}
return s.opts.PublicUrl
}
func (s *Server) prefixedPath(path string) string {
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return s.configuredBasePrefix() + path
}
func (s *Server) cookiePath() string {
basePrefix := s.configuredBasePrefix()
if basePrefix == "" {
return "/"
}
return basePrefix + "/"
}
func (s *Server) renderOptions() web.Options {
return web.Options{
BasePrefix: s.configuredBasePrefix(),
PublicURL: s.configuredPublicURL(),
}
}
func (s *Server) getTokenKeyfunc() jwt.Keyfunc { func (s *Server) getTokenKeyfunc() jwt.Keyfunc {
// return ED25519 key if HS512 is not set // return ED25519 key if HS512 is not set
if len(s.hs512Secret) == 0 { if len(s.hs512Secret) == 0 {
@@ -225,7 +141,7 @@ func (s *Server) issueChallenge(ctx context.Context, r *http.Request, lg *slog.L
return nil, err return nil, err
} }
lg.Info("new challenge issued", "challenge", id.String(), "weight", cr.Weight) lg.Info("new challenge issued", "challenge", id.String())
return &chall, err return &chall, err
} }
@@ -277,7 +193,7 @@ func (s *Server) maybeReverseProxyOrPage(w http.ResponseWriter, r *http.Request)
} }
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) { func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
lg, r := s.getRequestLogger(r) lg := internal.GetRequestLogger(s.logger, r)
if val, _ := s.store.Get(r.Context(), fmt.Sprintf("ogtags:allow:%s%s", r.Host, r.URL.String())); val != nil { if val, _ := s.store.Get(r.Context(), fmt.Sprintf("ogtags:allow:%s%s", r.Host, r.URL.String())); val != nil {
lg.Debug("serving opengraph tag asset") lg.Debug("serving opengraph tag asset")
@@ -285,7 +201,11 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
return return
} }
cookiePath := s.cookiePath() // Adjust cookie path if base prefix is not empty
cookiePath := "/"
if anubis.BasePrefix != "" {
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
}
cr, rule, err := s.check(r, lg) cr, rule, err := s.check(r, lg)
if err != nil { if err != nil {
@@ -298,10 +218,7 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
r.Header.Add("X-Anubis-Rule", cr.Name) r.Header.Add("X-Anubis-Rule", cr.Name)
r.Header.Add("X-Anubis-Action", string(cr.Rule)) r.Header.Add("X-Anubis-Action", string(cr.Rule))
lg = lg.With("check_result", cr) lg = lg.With("check_result", cr)
{ policy.Applications.WithLabelValues(cr.Name, string(cr.Rule)).Add(1)
asn, asnDesc := asnFromContext(r.Context())
policy.Applications.WithLabelValues(cr.Name, string(cr.Rule), asn, asnDesc).Add(1)
}
ip := r.Header.Get("X-Real-Ip") ip := r.Header.Get("X-Real-Ip")
@@ -379,7 +296,11 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
} }
func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.CheckResult, lg *slog.Logger, rule *policy.Bot) bool { func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.CheckResult, lg *slog.Logger, rule *policy.Bot) bool {
cookiePath := s.cookiePath() // Adjust cookie path if base prefix is not empty
cookiePath := "/"
if anubis.BasePrefix != "" {
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
}
localizer := localization.GetLocalizer(r) localizer := localization.GetLocalizer(r)
@@ -427,8 +348,7 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
lg.Error("can't look up ip in dnsbl", "err", err) lg.Error("can't look up ip in dnsbl", "err", err)
} }
db.Set(r.Context(), ip, resp, 24*time.Hour) db.Set(r.Context(), ip, resp, 24*time.Hour)
asn, asnDesc := asnFromContext(r.Context()) droneBLHits.WithLabelValues(resp.String()).Inc()
droneBLHits.WithLabelValues(resp.String(), asn, asnDesc).Inc()
} }
if resp != dnsbl.AllGood { if resp != dnsbl.AllGood {
@@ -446,7 +366,7 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
} }
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) { func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
lg, r := s.getRequestLogger(r) lg := internal.GetRequestLogger(s.logger, r)
localizer := localization.GetLocalizer(r) localizer := localization.GetLocalizer(r)
redir := r.FormValue("redir") redir := r.FormValue("redir")
@@ -515,14 +435,11 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
return return
} }
lg.Debug("made challenge", "challenge", chall, "rules", rule.Challenge, "cr", cr) lg.Debug("made challenge", "challenge", chall, "rules", rule.Challenge, "cr", cr)
{ challengesIssued.WithLabelValues("api").Inc()
asn, asnDesc := asnFromContext(r.Context())
challengesIssued.WithLabelValues("api", asn, asnDesc).Inc()
}
} }
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
lg, r := s.getRequestLogger(r) lg := internal.GetRequestLogger(s.logger, r)
localizer := localization.GetLocalizer(r) localizer := localization.GetLocalizer(r)
redir := r.FormValue("redir") redir := r.FormValue("redir")
@@ -542,7 +459,11 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
return return
} }
cookiePath := s.cookiePath() // Adjust cookie path if base prefix is not empty
cookiePath := "/"
if anubis.BasePrefix != "" {
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
}
if _, err := r.Cookie(anubis.TestCookieName); errors.Is(err, http.ErrNoCookie) { if _, err := r.Cookie(anubis.TestCookieName); errors.Is(err, http.ErrNoCookie) {
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host}) s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
@@ -609,8 +530,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
} }
if err := impl.Validate(r, lg, in); err != nil { if err := impl.Validate(r, lg, in); err != nil {
asn, asnDesc := asnFromContext(r.Context()) failedValidations.WithLabelValues(rule.Challenge.Algorithm).Inc()
failedValidations.WithLabelValues(rule.Challenge.Algorithm, asn, asnDesc).Inc()
var cerr *challenge.Error var cerr *challenge.Error
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host}) s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
lg.Debug("challenge validate call failed", "err", err) lg.Debug("challenge validate call failed", "err", err)
@@ -670,10 +590,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
lg.Debug("can't update information about challenge", "err", err) lg.Debug("can't update information about challenge", "err", err)
} }
{ challengesValidated.WithLabelValues(rule.Challenge.Algorithm).Inc()
asn, asnDesc := asnFromContext(r.Context())
challengesValidated.WithLabelValues(rule.Challenge.Algorithm, asn, asnDesc).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)
} }
@@ -712,8 +629,7 @@ func (s *Server) check(r *http.Request, lg *slog.Logger) (policy.CheckResult, *p
return cr("bot/"+b.Name, b.Action, weight), &b, nil return cr("bot/"+b.Name, b.Action, weight), &b, nil
case config.RuleWeigh: case config.RuleWeigh:
lg.Debug("adjusting weight", "name", b.Name, "delta", b.Weight.Adjust) lg.Debug("adjusting weight", "name", b.Name, "delta", b.Weight.Adjust)
asn, asnDesc := asnFromContext(r.Context()) policy.Applications.WithLabelValues("bot/"+b.Name, "WEIGH").Add(1)
policy.Applications.WithLabelValues("bot/"+b.Name, "WEIGH", asn, asnDesc).Add(1)
weight += b.Weight.Adjust weight += b.Weight.Adjust
} }
} }
+4 -94
View File
@@ -2,7 +2,6 @@ package lib
import ( import (
"bytes" "bytes"
"compress/gzip"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -59,7 +58,7 @@ func loadPolicies(t *testing.T, fname string, difficulty int) *policy.ParsedConf
t.Logf("loading policy file: %s", fname) t.Logf("loading policy file: %s", fname)
anubisPolicy, err := LoadPoliciesOrDefault(ctx, fname, difficulty, "info", false) anubisPolicy, err := LoadPoliciesOrDefault(ctx, fname, difficulty, "info")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -251,7 +250,7 @@ func TestLoadPolicies(t *testing.T) {
} }
defer fin.Close() defer fin.Close()
if _, err := policy.ParseConfig(t.Context(), fin, fname, 4, "info", false); err != nil { if _, err := policy.ParseConfig(t.Context(), fin, fname, 4, "info"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
}) })
@@ -502,11 +501,8 @@ func TestBasePrefix(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
originalBasePrefix := anubis.BasePrefix // Reset the global BasePrefix before each test
anubis.BasePrefix = "/not-this-server" anubis.BasePrefix = ""
t.Cleanup(func() {
anubis.BasePrefix = originalBasePrefix
})
pol := loadPolicies(t, "", 4) pol := loadPolicies(t, "", 4)
@@ -635,96 +631,10 @@ func TestBasePrefix(t *testing.T) {
if ckie.Path != expectedPath { if ckie.Path != expectedPath {
t.Errorf("cookie path is wrong, wanted %s, got: %s", expectedPath, ckie.Path) t.Errorf("cookie path is wrong, wanted %s, got: %s", expectedPath, ckie.Path)
} }
if anubis.BasePrefix != "/not-this-server" {
t.Errorf("New should not overwrite anubis.BasePrefix, got %q", anubis.BasePrefix)
}
}) })
} }
} }
func TestBasePrefixUsesServerConfigNotPackageGlobal(t *testing.T) {
originalBasePrefix := anubis.BasePrefix
anubis.BasePrefix = "/global"
t.Cleanup(func() {
anubis.BasePrefix = originalBasePrefix
})
pol := loadPolicies(t, "", 4)
srv := spawnAnubis(t, Options{
Next: http.NewServeMux(),
Policy: pol,
BasePrefix: "/local/",
})
if anubis.BasePrefix != "/global" {
t.Fatalf("New should leave anubis.BasePrefix alone, got %q", anubis.BasePrefix)
}
req := httptest.NewRequest(http.MethodPost, "/local/.within.website/x/cmd/anubis/api/make-challenge?redir=/", nil)
req.Header.Set("X-Real-Ip", "127.0.0.1")
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected local server prefix route to work, got status %d body %q", rr.Code, rr.Body.String())
}
}
func TestRenderIndexUsesServerBasePrefixNotPackageGlobal(t *testing.T) {
originalBasePrefix := anubis.BasePrefix
anubis.BasePrefix = "/global"
t.Cleanup(func() {
anubis.BasePrefix = originalBasePrefix
})
pol := loadPolicies(t, "", 4)
srv := spawnAnubis(t, Options{
Next: http.NewServeMux(),
Policy: pol,
BasePrefix: "/local/",
})
req := httptest.NewRequest(http.MethodGet, "/local/protected", nil)
req.Header.Set("X-Real-Ip", "127.0.0.1")
req.Header.Set("User-Agent", "Mozilla/5.0")
req.Header.Set("Accept-Encoding", "gzip")
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
resp := rr.Result()
defer resp.Body.Close()
var body strings.Builder
reader := io.Reader(resp.Body)
if resp.Header.Get("Content-Encoding") == "gzip" {
gzipReader, err := gzip.NewReader(resp.Body)
if err != nil {
t.Fatalf("opening gzip response should not fail: %v", err)
}
defer gzipReader.Close()
reader = gzipReader
}
if _, err := io.Copy(&body, reader); err != nil {
t.Fatalf("reading challenge response should not fail: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected challenge status %d, got %d body %q", http.StatusOK, resp.StatusCode, body.String())
}
if !strings.Contains(body.String(), "/local/.within.website/x/cmd/anubis/static/js/main.mjs") {
t.Fatalf("expected challenge assets to use server base prefix, body %q", body.String())
}
if strings.Contains(body.String(), "/global/.within.website") {
t.Fatalf("challenge body used package global base prefix: %q", body.String())
}
if anubis.BasePrefix != "/global" {
t.Fatalf("rendering should leave anubis.BasePrefix alone, got %q", anubis.BasePrefix)
}
}
func TestCustomStatusCodes(t *testing.T) { func TestCustomStatusCodes(t *testing.T) {
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Log(r.UserAgent()) t.Log(r.UserAgent())
-1
View File
@@ -44,7 +44,6 @@ func Methods() []string {
} }
type IssueInput struct { type IssueInput struct {
BasePrefix string
Impressum *config.Impressum Impressum *config.Impressum
Rule *policy.Bot Rule *policy.Bot
Challenge *Challenge Challenge *Challenge
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/lib/challenge" "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
"github.com/a-h/templ" "github.com/a-h/templ"
@@ -27,7 +28,7 @@ func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in
return nil, err return nil, err
} }
u, err := r.URL.Parse(in.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge") u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge")
if err != nil { if err != nil {
return nil, fmt.Errorf("can't render page: %w", err) return nil, fmt.Errorf("can't render page: %w", err)
} }
@@ -46,7 +47,7 @@ func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in
loc := localization.GetLocalizer(r) loc := localization.GetLocalizer(r)
result := page(in.BasePrefix, u.String(), in.Rule.Challenge.Difficulty, showMeta, loc) result := page(u.String(), in.Rule.Challenge.Difficulty, showMeta, loc)
return result, nil return result, nil
} }
+3 -3
View File
@@ -7,10 +7,10 @@ import (
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
) )
templ page(basePrefix, redir string, difficulty int, showMeta bool, loc *localization.SimpleLocalizer) { templ page(redir string, difficulty int, showMeta bool, loc *localization.SimpleLocalizer) {
<div class="centered-div"> <div class="centered-div">
<img id="image" style="width:100%;max-width:256px;" src={ basePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/> <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={ basePrefix + "/.within.website/x/cmd/anubis/static/img/happy.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 }/>
<p id="status">{ loc.T("loading") }</p> <p id="status">{ loc.T("loading") }</p>
<p>{ loc.T("connection_security") }</p> <p>{ loc.T("connection_security") }</p>
if showMeta { if showMeta {
+5 -5
View File
@@ -15,7 +15,7 @@ import (
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
) )
func page(basePrefix, redir string, difficulty int, showMeta bool, loc *localization.SimpleLocalizer) templ.Component { func page(redir string, difficulty int, showMeta bool, loc *localization.SimpleLocalizer) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -41,9 +41,9 @@ func page(basePrefix, redir string, difficulty int, showMeta bool, loc *localiza
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(basePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 12, Col: 158} return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 12, Col: 165}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -54,9 +54,9 @@ func page(basePrefix, redir string, difficulty int, showMeta bool, loc *localiza
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(basePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 13, Col: 167} return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 13, Col: 174}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
+3 -2
View File
@@ -10,6 +10,7 @@ import (
"net/http" "net/http"
"time" "time"
"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/challenge"
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
@@ -42,7 +43,7 @@ func (i *impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in
return nil, err return nil, err
} }
u, err := r.URL.Parse(in.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge") u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge")
if err != nil { if err != nil {
return nil, fmt.Errorf("can't render page: %w", err) return nil, fmt.Errorf("can't render page: %w", err)
} }
@@ -54,7 +55,7 @@ func (i *impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in
loc := localization.GetLocalizer(r) loc := localization.GetLocalizer(r)
result := page(in.BasePrefix, u.String(), in.Challenge.RandomData, in.Rule.Challenge.Difficulty, loc) result := page(u.String(), in.Challenge.RandomData, in.Rule.Challenge.Difficulty, loc)
return result, nil return result, nil
} }
+3 -3
View File
@@ -5,10 +5,10 @@ import (
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
) )
templ page(basePrefix, redir, challenge string, difficulty int, loc *localization.SimpleLocalizer) { templ page(redir, challenge string, difficulty int, loc *localization.SimpleLocalizer) {
<div class="centered-div"> <div class="centered-div">
<div id="app"> <div id="app">
<img id="image" style="width:100%;max-width:256px;" src={ basePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/> <img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/>
<p id="status">{ loc.T("loading") }</p> <p id="status">{ loc.T("loading") }</p>
<p>{ loc.T("connection_security") }</p> <p>{ loc.T("connection_security") }</p>
</div> </div>
@@ -18,7 +18,7 @@ templ page(basePrefix, redir, challenge string, difficulty int, loc *localizatio
"difficulty": difficulty, "difficulty": difficulty,
"connection_security_message": loc.T("connection_security"), "connection_security_message": loc.T("connection_security"),
"loading_message": loc.T("loading"), "loading_message": loc.T("loading"),
"pensive_url": basePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version, "pensive_url": anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version,
}) })
@templ.ComponentFunc(renderAppJS) @templ.ComponentFunc(renderAppJS)
<noscript> <noscript>
+4 -4
View File
@@ -13,7 +13,7 @@ import (
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
) )
func page(basePrefix, redir, challenge string, difficulty int, loc *localization.SimpleLocalizer) templ.Component { func page(redir, challenge string, difficulty int, loc *localization.SimpleLocalizer) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -39,9 +39,9 @@ func page(basePrefix, redir, challenge string, difficulty int, loc *localization
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(basePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `preact.templ`, Line: 11, Col: 159} return templ.Error{Err: templ_7745c5c3_Err, FileName: `preact.templ`, Line: 11, Col: 166}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -83,7 +83,7 @@ func page(basePrefix, redir, challenge string, difficulty int, loc *localization
"difficulty": difficulty, "difficulty": difficulty,
"connection_security_message": loc.T("connection_security"), "connection_security_message": loc.T("connection_security"),
"loading_message": loc.T("loading"), "loading_message": loc.T("loading"),
"pensive_url": basePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version, "pensive_url": anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version,
}).Render(ctx, templ_7745c5c3_Buffer) }).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
+1 -1
View File
@@ -29,7 +29,7 @@ func (i *Impl) Setup(mux *http.ServeMux) {}
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) { func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
loc := localization.GetLocalizer(r) loc := localization.GetLocalizer(r)
return page(in.BasePrefix, loc), nil return page(loc), nil
} }
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error { func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
+4 -4
View File
@@ -5,12 +5,12 @@ import (
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
) )
templ page(basePrefix string, localizer *localization.SimpleLocalizer) { templ page(localizer *localization.SimpleLocalizer) {
<div class="centered-div"> <div class="centered-div">
<img id="image" style="width:100%;max-width:256px;" src={ basePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/> <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={ basePrefix + "/.within.website/x/cmd/anubis/static/img/happy.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 }/>
<p id="status">{ localizer.T("loading") }</p> <p id="status">{ localizer.T("loading") }</p>
<script async type="module" src={ basePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script> <script async type="module" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script>
<div id="progress" role="progressbar" aria-labelledby="status"> <div id="progress" role="progressbar" aria-labelledby="status">
<div class="bar-inner"></div> <div class="bar-inner"></div>
</div> </div>
+7 -7
View File
@@ -13,7 +13,7 @@ import (
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
) )
func page(basePrefix string, localizer *localization.SimpleLocalizer) templ.Component { func page(localizer *localization.SimpleLocalizer) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -39,9 +39,9 @@ func page(basePrefix string, localizer *localization.SimpleLocalizer) templ.Comp
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(basePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 10, Col: 158} return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 10, Col: 165}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -52,9 +52,9 @@ func page(basePrefix string, localizer *localization.SimpleLocalizer) templ.Comp
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(basePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 11, Col: 167} return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 11, Col: 174}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -78,9 +78,9 @@ func page(basePrefix string, localizer *localization.SimpleLocalizer) templ.Comp
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(basePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 13, Col: 129} return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 13, Col: 136}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
+28 -14
View File
@@ -55,7 +55,7 @@ type Options struct {
DifficultyInJWT bool DifficultyInJWT bool
} }
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string, subrequestMode bool) (*policy.ParsedConfig, error) { func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string) (*policy.ParsedConfig, error) {
var fin io.ReadCloser var fin io.ReadCloser
var err error var err error
@@ -79,7 +79,7 @@ func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty
} }
}(fin) }(fin)
anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty, logLevel, subrequestMode) anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty, logLevel)
if err != nil { if err != nil {
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err) return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
} }
@@ -112,7 +112,8 @@ func New(opts Options) (*Server, error) {
opts.ED25519PrivateKey = priv opts.ED25519PrivateKey = priv
} }
opts.BasePrefix = strings.TrimRight(opts.BasePrefix, "/") anubis.BasePrefix = strings.TrimRight(opts.BasePrefix, "/")
anubis.PublicUrl = opts.PublicUrl
result := &Server{ result := &Server{
next: opts.Next, next: opts.Next,
@@ -120,8 +121,6 @@ func New(opts Options) (*Server, error) {
hs512Secret: opts.HS512Secret, hs512Secret: opts.HS512Secret,
policy: opts.Policy, policy: opts.Policy,
opts: opts, opts: opts,
basePrefix: opts.BasePrefix,
publicURL: opts.PublicUrl,
OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph, opts.Policy.Store, ogtags.TargetOptions{ OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph, opts.Policy.Store, ogtags.TargetOptions{
Host: opts.TargetHost, Host: opts.TargetHost,
SNI: opts.TargetSNI, SNI: opts.TargetSNI,
@@ -132,20 +131,28 @@ func New(opts Options) (*Server, error) {
} }
mux := http.NewServeMux() mux := http.NewServeMux()
xessPrefix := result.prefixedPath(xess.BasePrefix) xess.Mount(mux)
mux.Handle(xessPrefix, internal.UnchangingCache(http.StripPrefix(xessPrefix, http.FileServerFS(xess.Static))))
// Helper to add the server-local base prefix. // Helper to add global prefix
registerWithPrefix := func(pattern string, handler http.Handler, method string) { registerWithPrefix := func(pattern string, handler http.Handler, method string) {
if method != "" { if method != "" {
method = method + " " // methods must end with a space to register with them method = method + " " // methods must end with a space to register with them
} }
mux.Handle(method+result.prefixedPath(pattern), handler) // Ensure there's no double slash when concatenating BasePrefix and pattern
basePrefix := strings.TrimSuffix(anubis.BasePrefix, "/")
prefix := method + basePrefix
// If pattern doesn't start with a slash, add one
if !strings.HasPrefix(pattern, "/") {
pattern = "/" + pattern
}
mux.Handle(prefix+pattern, handler)
} }
// Ensure there's no double slash when concatenating BasePrefix and StaticPath // Ensure there's no double slash when concatenating BasePrefix and StaticPath
stripPrefix := result.prefixedPath(anubis.StaticPath) stripPrefix := strings.TrimSuffix(anubis.BasePrefix, "/") + anubis.StaticPath
registerWithPrefix(anubis.StaticPath, internal.UnchangingCache(internal.NoBrowsing(http.StripPrefix(stripPrefix, http.FileServerFS(web.Static)))), "") registerWithPrefix(anubis.StaticPath, internal.UnchangingCache(internal.NoBrowsing(http.StripPrefix(stripPrefix, http.FileServerFS(web.Static)))), "")
if opts.ServeRobotsTXT { if opts.ServeRobotsTXT {
@@ -159,10 +166,9 @@ func New(opts Options) (*Server, error) {
if opts.Policy.Impressum != nil { if opts.Policy.Impressum != nil {
registerWithPrefix(anubis.APIPrefix+"imprint", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { registerWithPrefix(anubis.APIPrefix+"imprint", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handler := templ.Handler( templ.Handler(
web.BaseWithOptions(result.renderOptions(), opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum, localization.GetLocalizer(r)), web.Base(opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum, localization.GetLocalizer(r)),
) ).ServeHTTP(w, r)
handler.ServeHTTP(w, r)
}), "GET") }), "GET")
} }
@@ -184,6 +190,14 @@ func New(opts Options) (*Server, error) {
}, },
Name: "honeypot/network", Name: "honeypot/network",
}, },
policy.Bot{
Rules: mazeGen.CheckUA(),
Action: config.RuleWeigh,
Weight: &config.Weight{
Adjust: 30,
},
Name: "honeypot/user-agent",
},
) )
} else { } else {
result.logger.Error("can't init honeypot subsystem", "err", err) result.logger.Error("can't init honeypot subsystem", "err", err)
-1
View File
@@ -17,7 +17,6 @@ type Logging struct {
Sink string `json:"sink"` // Logging sink, either "stdio" or "file" Sink string `json:"sink"` // Logging sink, either "stdio" or "file"
Level *slog.Level `json:"level"` // Log level, if set supersedes the level in flags Level *slog.Level `json:"level"` // Log level, if set supersedes the level in flags
Parameters *LoggingFileConfig `json:"parameters"` // Logging parameters, to be dynamic in the future Parameters *LoggingFileConfig `json:"parameters"` // Logging parameters, to be dynamic in the future
LogASN bool `json:"asn" yaml:"asn"`
} }
const ( const (
-33
View File
@@ -12,7 +12,6 @@ import (
var ( var (
ErrInvalidMetricsConfig = errors.New("config: invalid metrics configuration") ErrInvalidMetricsConfig = errors.New("config: invalid metrics configuration")
ErrInvalidMetricsTLSConfig = errors.New("config: invalid metrics TLS configuration") ErrInvalidMetricsTLSConfig = errors.New("config: invalid metrics TLS configuration")
ErrInvalidMetricsBasicAuthConfig = errors.New("config: invalid metrics basic auth configuration")
ErrNoMetricsBind = errors.New("config.Metrics: must define bind") ErrNoMetricsBind = errors.New("config.Metrics: must define bind")
ErrNoMetricsNetwork = errors.New("config.Metrics: must define network") ErrNoMetricsNetwork = errors.New("config.Metrics: must define network")
ErrNoMetricsSocketMode = errors.New("config.Metrics: must define socket mode when using unix sockets") ErrNoMetricsSocketMode = errors.New("config.Metrics: must define socket mode when using unix sockets")
@@ -23,8 +22,6 @@ var (
ErrInvalidMetricsTLSKeypair = errors.New("config.Metrics.TLS: keypair is invalid") ErrInvalidMetricsTLSKeypair = errors.New("config.Metrics.TLS: keypair is invalid")
ErrInvalidMetricsCACertificate = errors.New("config.Metrics.TLS: invalid CA certificate") ErrInvalidMetricsCACertificate = errors.New("config.Metrics.TLS: invalid CA certificate")
ErrCantReadFile = errors.New("config: can't read required file") ErrCantReadFile = errors.New("config: can't read required file")
ErrNoMetricsBasicAuthUsername = errors.New("config.Metrics.BasicAuth: must define username")
ErrNoMetricsBasicAuthPassword = errors.New("config.Metrics.BasicAuth: must define password")
) )
type Metrics struct { type Metrics struct {
@@ -32,7 +29,6 @@ type Metrics struct {
Network string `json:"network" yaml:"network"` Network string `json:"network" yaml:"network"`
SocketMode string `json:"socketMode" yaml:"socketMode"` SocketMode string `json:"socketMode" yaml:"socketMode"`
TLS *MetricsTLS `json:"tls" yaml:"tls"` TLS *MetricsTLS `json:"tls" yaml:"tls"`
BasicAuth *MetricsBasicAuth `json:"basicAuth" yaml:"basicAuth"`
} }
func (m *Metrics) Valid() error { func (m *Metrics) Valid() error {
@@ -66,12 +62,6 @@ func (m *Metrics) Valid() error {
} }
} }
if m.BasicAuth != nil {
if err := m.BasicAuth.Valid(); err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 { if len(errs) != 0 {
return errors.Join(ErrInvalidMetricsConfig, errors.Join(errs...)) return errors.Join(ErrInvalidMetricsConfig, errors.Join(errs...))
} }
@@ -141,26 +131,3 @@ func canReadFile(fname string) error {
return nil return nil
} }
type MetricsBasicAuth struct {
Username string `json:"username" yaml:"username"`
Password string `json:"password" yaml:"password"`
}
func (mba *MetricsBasicAuth) Valid() error {
var errs []error
if mba.Username == "" {
errs = append(errs, ErrNoMetricsBasicAuthUsername)
}
if mba.Password == "" {
errs = append(errs, ErrNoMetricsBasicAuthPassword)
}
if len(errs) != 0 {
return errors.Join(ErrInvalidMetricsBasicAuthConfig, errors.Join(errs...))
}
return nil
}
-73
View File
@@ -157,79 +157,6 @@ func TestMetricsValid(t *testing.T) {
}, },
err: ErrInvalidMetricsCACertificate, err: ErrInvalidMetricsCACertificate,
}, },
{
name: "basic auth credentials set",
input: &Metrics{
Bind: ":9090",
Network: "tcp",
BasicAuth: &MetricsBasicAuth{
Username: "admin",
Password: "hunter2",
},
},
},
{
name: "invalid basic auth config",
input: &Metrics{
Bind: ":9090",
Network: "tcp",
BasicAuth: &MetricsBasicAuth{},
},
err: ErrInvalidMetricsBasicAuthConfig,
},
} {
t.Run(tt.name, func(t *testing.T) {
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
t.Logf("wanted error: %v", tt.err)
t.Logf("got error: %v", err)
t.Error("validation failed")
}
})
}
}
func TestMetricsBasicAuthValid(t *testing.T) {
for _, tt := range []struct {
name string
input *MetricsBasicAuth
err error
}{
{
name: "both set",
input: &MetricsBasicAuth{
Username: "admin",
Password: "hunter2",
},
},
{
name: "empty username and password",
input: &MetricsBasicAuth{},
err: ErrInvalidMetricsBasicAuthConfig,
},
{
name: "missing username",
input: &MetricsBasicAuth{
Password: "hunter2",
},
err: ErrNoMetricsBasicAuthUsername,
},
{
name: "missing password",
input: &MetricsBasicAuth{
Username: "admin",
},
err: ErrNoMetricsBasicAuthPassword,
},
{
name: "missing both surfaces wrapper error",
input: &MetricsBasicAuth{},
err: ErrNoMetricsBasicAuthUsername,
},
{
name: "missing both surfaces password error",
input: &MetricsBasicAuth{},
err: ErrNoMetricsBasicAuthPassword,
},
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if err := tt.input.Valid(); !errors.Is(err, tt.err) { if err := tt.input.Valid(); !errors.Is(err, tt.err) {
+4 -4
View File
@@ -12,7 +12,7 @@ import (
) )
func TestInvalidChallengeMethod(t *testing.T) { func TestInvalidChallengeMethod(t *testing.T) {
if _, err := LoadPoliciesOrDefault(t.Context(), "testdata/invalid-challenge-method.yaml", 4, "info", false); !errors.Is(err, policy.ErrChallengeRuleHasWrongAlgorithm) { if _, err := LoadPoliciesOrDefault(t.Context(), "testdata/invalid-challenge-method.yaml", 4, "info"); !errors.Is(err, policy.ErrChallengeRuleHasWrongAlgorithm) {
t.Fatalf("wanted error %v but got %v", policy.ErrChallengeRuleHasWrongAlgorithm, err) t.Fatalf("wanted error %v but got %v", policy.ErrChallengeRuleHasWrongAlgorithm, err)
} }
} }
@@ -25,7 +25,7 @@ func TestBadConfigs(t *testing.T) {
for _, st := range finfos { for _, st := range finfos {
t.Run(st.Name(), func(t *testing.T) { t.Run(st.Name(), func(t *testing.T) {
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "bad", st.Name()), anubis.DefaultDifficulty, "info", false); err == nil { if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "bad", st.Name()), anubis.DefaultDifficulty, "info"); err == nil {
t.Fatal(err) t.Fatal(err)
} else { } else {
t.Log(err) t.Log(err)
@@ -44,13 +44,13 @@ func TestGoodConfigs(t *testing.T) {
t.Run(st.Name(), func(t *testing.T) { t.Run(st.Name(), func(t *testing.T) {
t.Run("with-thoth", func(t *testing.T) { t.Run("with-thoth", func(t *testing.T) {
ctx := thothmock.WithMockThoth(t) ctx := thothmock.WithMockThoth(t)
if _, err := LoadPoliciesOrDefault(ctx, filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info", false); err != nil { if _, err := LoadPoliciesOrDefault(ctx, filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
}) })
t.Run("without-thoth", func(t *testing.T) { t.Run("without-thoth", func(t *testing.T) {
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info", false); err != nil { if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
}) })
+23 -43
View File
@@ -193,7 +193,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
localizer := localization.GetLocalizer(r) localizer := localization.GetLocalizer(r)
if returnHTTPStatusOnly { if returnHTTPStatusOnly {
if s.configuredPublicURL() == "" { if s.opts.PublicUrl == "" {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(localizer.T("authorization_required"))) w.Write([]byte(localizer.T("authorization_required")))
} else { } else {
@@ -207,7 +207,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
return return
} }
lg, r := s.getRequestLogger(r) lg := internal.GetRequestLogger(s.logger, r)
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) { if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) {
lg.Error("client was given a challenge but does not in fact support gzip compression") lg.Error("client was given a challenge but does not in fact support gzip compression")
@@ -215,10 +215,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
return return
} }
{ challengesIssued.WithLabelValues("embedded").Add(1)
asn, asnDesc := asnFromContext(r.Context())
challengesIssued.WithLabelValues("embedded", asn, asnDesc).Add(1)
}
chall, err := s.issueChallenge(r.Context(), r, lg, cr, rule) chall, err := s.issueChallenge(r.Context(), r, lg, cr, rule)
if err != nil { if err != nil {
lg.Error("can't get challenge", "err", err) lg.Error("can't get challenge", "err", err)
@@ -263,7 +260,6 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
} }
in := &challenge.IssueInput{ in := &challenge.IssueInput{
BasePrefix: s.configuredBasePrefix(),
Impressum: s.policy.Impressum, Impressum: s.policy.Impressum,
Rule: rule, Rule: rule,
Challenge: chall, Challenge: chall,
@@ -278,8 +274,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
return return
} }
page := web.BaseWithChallengeAndOGTagsWithOptions( page := web.BaseWithChallengeAndOGTags(
s.renderOptions(),
localizer.T("making_sure_not_bot"), localizer.T("making_sure_not_bot"),
component, component,
s.policy.Impressum, s.policy.Impressum,
@@ -311,31 +306,29 @@ func (s *Server) constructRedirectURL(r *http.Request) (string, error) {
case "http", "https": case "http", "https":
// allowed // allowed
default: default:
lg, _ := s.getRequestLogger(r) lg := internal.GetRequestLogger(s.logger, r)
lg.Warn("invalid protocol in X-Forwarded-Proto", "proto", proto) lg.Warn("invalid protocol in X-Forwarded-Proto", "proto", proto)
return "", errors.New(localizer.T("invalid_redirect")) return "", errors.New(localizer.T("invalid_redirect"))
} }
// Check if host is allowed in RedirectDomains (supports '*' via glob) // Check if host is allowed in RedirectDomains (supports '*' via glob)
if len(s.opts.RedirectDomains) > 0 && !matchRedirectDomain(s.opts.RedirectDomains, host) { if len(s.opts.RedirectDomains) > 0 && !matchRedirectDomain(s.opts.RedirectDomains, host) {
lg, _ := s.getRequestLogger(r) lg := internal.GetRequestLogger(s.logger, r)
lg.Debug("domain not allowed", "domain", host) lg.Debug("domain not allowed", "domain", host)
return "", errors.New(localizer.T("redirect_domain_not_allowed")) return "", errors.New(localizer.T("redirect_domain_not_allowed"))
} }
redir := proto + "://" + host + uri redir := proto + "://" + host + uri
escapedURL := url.QueryEscape(redir) escapedURL := url.QueryEscape(redir)
return fmt.Sprintf("%s/.within.website/?redir=%s", s.configuredPublicURL(), escapedURL), nil return fmt.Sprintf("%s/.within.website/?redir=%s", s.opts.PublicUrl, escapedURL), nil
} }
func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) { func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) {
localizer := localization.GetLocalizer(r) localizer := localization.GetLocalizer(r)
opts := s.renderOptions()
handler := templ.Handler( templ.Handler(
web.BaseWithOptions(opts, localizer.T("benchmarking_anubis"), web.BenchWithOptions(opts, localizer), s.policy.Impressum, localizer), web.Base(localizer.T("benchmarking_anubis"), web.Bench(localizer), s.policy.Impressum, localizer),
) ).ServeHTTP(w, r)
handler.ServeHTTP(w, r)
} }
func (s *Server) respondWithError(w http.ResponseWriter, r *http.Request, message, code string) { func (s *Server) respondWithError(w http.ResponseWriter, r *http.Request, message, code string) {
@@ -344,12 +337,10 @@ func (s *Server) respondWithError(w http.ResponseWriter, r *http.Request, messag
func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg, code string, status int) { func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg, code string, status int) {
localizer := localization.GetLocalizer(r) localizer := localization.GetLocalizer(r)
opts := s.renderOptions()
component := web.BaseWithOptions( component := web.Base(
opts,
localizer.T("oh_noes"), localizer.T("oh_noes"),
web.ErrorPageWithOptions(opts, msg, s.opts.WebmasterEmail, code, localizer), web.ErrorPage(msg, s.opts.WebmasterEmail, code, localizer),
s.policy.Impressum, s.policy.Impressum,
localizer, localizer,
) )
@@ -358,25 +349,17 @@ func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg,
} }
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, s.prefixedPath(anubis.StaticPath)) { if strings.HasPrefix(r.URL.Path, anubis.BasePrefix+anubis.StaticPath) {
s.mux.ServeHTTP(w, r) s.mux.ServeHTTP(w, r)
return return
} else if strings.HasPrefix(r.URL.Path, s.prefixedPath(xess.BasePrefix)) { } else if strings.HasPrefix(r.URL.Path, anubis.BasePrefix+xess.BasePrefix) {
s.mux.ServeHTTP(w, r) s.mux.ServeHTTP(w, r)
return return
} }
// Forward robots.txt requests to mux when ServeRobotsTXT is enabled // Forward robots.txt requests to mux when ServeRobotsTXT is enabled
if s.opts.ServeRobotsTXT { if s.opts.ServeRobotsTXT {
path := r.URL.Path path := strings.TrimPrefix(r.URL.Path, anubis.BasePrefix)
basePrefix := s.configuredBasePrefix()
if basePrefix != "" {
if !strings.HasPrefix(path, basePrefix) {
s.maybeReverseProxyOrPage(w, r)
return
}
path = strings.TrimPrefix(path, basePrefix)
}
if path == "/robots.txt" || path == "/.well-known/robots.txt" { if path == "/robots.txt" || path == "/.well-known/robots.txt" {
s.mux.ServeHTTP(w, r) s.mux.ServeHTTP(w, r)
return return
@@ -387,11 +370,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
func (s *Server) stripBasePrefixFromRequest(r *http.Request) *http.Request { func (s *Server) stripBasePrefixFromRequest(r *http.Request) *http.Request {
basePrefix := s.configuredBasePrefix() if !s.opts.StripBasePrefix || s.opts.BasePrefix == "" {
if !s.opts.StripBasePrefix || basePrefix == "" {
return r return r
} }
basePrefix := strings.TrimSuffix(s.opts.BasePrefix, "/")
path := r.URL.Path path := r.URL.Path
if !strings.HasPrefix(path, basePrefix) { if !strings.HasPrefix(path, basePrefix) {
@@ -432,7 +415,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
case "", "http", "https": case "", "http", "https":
// allowed: empty scheme means relative URL // allowed: empty scheme means relative URL
default: default:
lg, _ := s.getRequestLogger(r) lg := internal.GetRequestLogger(s.logger, r)
lg.Warn("XSS attempt blocked, invalid redirect scheme", "scheme", urlParsed.Scheme, "redir", redir) lg.Warn("XSS attempt blocked, invalid redirect scheme", "scheme", urlParsed.Scheme, "redir", redir)
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), "", http.StatusBadRequest) s.respondWithStatus(w, r, localizer.T("invalid_redirect"), "", http.StatusBadRequest)
return return
@@ -444,7 +427,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
hostMismatch := r.URL.Host != "" && urlParsed.Host != "" && urlParsed.Host != r.URL.Host hostMismatch := r.URL.Host != "" && urlParsed.Host != "" && urlParsed.Host != r.URL.Host
if hostNotAllowed || hostMismatch { if hostNotAllowed || hostMismatch {
lg, _ := s.getRequestLogger(r) lg := internal.GetRequestLogger(s.logger, r)
lg.Debug("domain not allowed", "domain", urlParsed.Host) lg.Debug("domain not allowed", "domain", urlParsed.Host)
s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), makeCode(err), http.StatusBadRequest) s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), makeCode(err), http.StatusBadRequest)
return return
@@ -455,14 +438,11 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
return return
} }
opts := s.renderOptions() templ.Handler(
handler := templ.Handler( web.Base(localizer.T("you_are_not_a_bot"), web.StaticHappy(localizer), s.policy.Impressum, localizer),
web.BaseWithOptions(opts, localizer.T("you_are_not_a_bot"), web.StaticHappyWithOptions(opts, localizer), s.policy.Impressum, localizer), ).ServeHTTP(w, r)
)
handler.ServeHTTP(w, r)
} else { } else {
asn, asnDesc := asnFromContext(r.Context()) requestsProxied.WithLabelValues(r.Host).Inc()
requestsProxied.WithLabelValues(r.Host, asn, asnDesc).Inc()
r = s.stripBasePrefixFromRequest(r) r = s.stripBasePrefixFromRequest(r)
s.next.ServeHTTP(w, r) s.next.ServeHTTP(w, r)
} }
+1 -8
View File
@@ -64,7 +64,7 @@ func (s *Server) run(ctx context.Context, lg *slog.Logger) error {
ErrorLog: internal.GetFilteredHTTPLogger(), ErrorLog: internal.GetFilteredHTTPLogger(),
} }
ln, metricsURL, err := internal.SetupListener(s.Config.Network, s.Config.Bind, s.Config.SocketMode) ln, metricsURL, err := internal.SetupListener(s.Config.Bind, s.Config.Network, s.Config.SocketMode)
if err != nil { if err != nil {
return fmt.Errorf("can't setup listener: %w", err) return fmt.Errorf("can't setup listener: %w", err)
} }
@@ -97,13 +97,6 @@ func (s *Server) run(ctx context.Context, lg *slog.Logger) error {
} }
} }
if s.Config.BasicAuth != nil {
var h http.Handler = mux
h = internal.BasicAuth("anubis-metrics", s.Config.BasicAuth.Username, s.Config.BasicAuth.Password, mux)
srv.Handler = h
}
lg.Debug("listening for metrics", "url", metricsURL) lg.Debug("listening for metrics", "url", metricsURL)
go func() { go func() {
+2 -13
View File
@@ -15,10 +15,9 @@ import (
type CELChecker struct { type CELChecker struct {
program cel.Program program cel.Program
src string src string
subRequestMode bool
} }
func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns, subRequestMode bool) (*CELChecker, error) { func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns) (*CELChecker, error) {
env, err := expressions.BotEnvironment(dnsObj) env, err := expressions.BotEnvironment(dnsObj)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -32,7 +31,6 @@ func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns, subRequestMode
return &CELChecker{ return &CELChecker{
src: cfg.String(), src: cfg.String(),
program: program, program: program,
subRequestMode: subRequestMode,
}, nil }, nil
} }
@@ -41,7 +39,7 @@ func (cc *CELChecker) Hash() string {
} }
func (cc *CELChecker) Check(r *http.Request) (bool, error) { func (cc *CELChecker) Check(r *http.Request) (bool, error) {
result, _, err := cc.program.ContextEval(r.Context(), &CELRequest{r, cc.subRequestMode}) result, _, err := cc.program.ContextEval(r.Context(), &CELRequest{r})
if err != nil { if err != nil {
return false, err return false, err
@@ -56,7 +54,6 @@ func (cc *CELChecker) Check(r *http.Request) (bool, error) {
type CELRequest struct { type CELRequest struct {
*http.Request *http.Request
subRequestMode bool
} }
func (cr *CELRequest) Parent() cel.Activation { return nil } func (cr *CELRequest) Parent() cel.Activation { return nil }
@@ -74,14 +71,6 @@ func (cr *CELRequest) ResolveName(name string) (any, bool) {
case "userAgent": case "userAgent":
return cr.UserAgent(), true return cr.UserAgent(), true
case "path": case "path":
if cr.subRequestMode {
if xou := cr.Header.Get("X-Original-URI"); xou != "" {
return xou, true
}
if xfu := cr.Header.Get("X-Forwarded-Uri"); xfu != "" {
return xfu, true
}
}
return cr.URL.Path, true return cr.URL.Path, true
case "query": case "query":
return expressions.URLValues{Values: cr.URL.Query()}, true return expressions.URLValues{Values: cr.URL.Query()}, true
+1 -75
View File
@@ -23,7 +23,7 @@ func TestCELChecker_MapIterationWrappers(t *testing.T) {
Expression: `headers.exists(k, k == "Accept") && query.exists(k, k == "format")`, Expression: `headers.exists(k, k == "Accept") && query.exists(k, k == "format")`,
} }
checker, err := NewCELChecker(cfg, newTestDNS(t), false) checker, err := NewCELChecker(cfg, newTestDNS(t))
if err != nil { if err != nil {
t.Fatalf("creating CEL checker failed: %v", err) t.Fatalf("creating CEL checker failed: %v", err)
} }
@@ -42,77 +42,3 @@ func TestCELChecker_MapIterationWrappers(t *testing.T) {
t.Fatal("expected expression to evaluate true") t.Fatal("expected expression to evaluate true")
} }
} }
func TestCELChecker_PathWithForwardedUri(t *testing.T) {
tests := []struct {
name string
expression string
xForwardedUri string
urlPath string
subRequestMode bool
want bool
}{
{
name: "path matches X-Forwarded-Uri in subrequest mode",
expression: `path.startsWith("/admin")`,
xForwardedUri: "/admin/secret",
urlPath: "/.within.website/x/cmd/anubis/api/check",
subRequestMode: true,
want: true,
},
{
name: "path with query string",
expression: `path.startsWith("/api/secret")`,
xForwardedUri: "/api/secret?token=abc",
urlPath: "/.within.website/x/cmd/anubis/api/check",
subRequestMode: true,
want: true,
},
{
name: "path falls back to url path when no header",
expression: `path == "/public/page"`,
urlPath: "/public/page",
subRequestMode: true,
want: true,
},
{
name: "non-subrequest mode ignores X-Forwarded-Uri",
expression: `path.startsWith("/admin")`,
xForwardedUri: "/admin/secret",
urlPath: "/public/page",
subRequestMode: false,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &config.ExpressionOrList{
Expression: tt.expression,
}
checker, err := NewCELChecker(cfg, newTestDNS(t), tt.subRequestMode)
if err != nil {
t.Fatalf("NewCELChecker() error: %v", err)
}
req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.urlPath, nil)
if err != nil {
t.Fatalf("http.NewRequest: %v", err)
}
if tt.xForwardedUri != "" {
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
}
got, err := checker.Check(req)
if err != nil {
t.Fatalf("Check() error: %v", err)
}
if got != tt.want {
t.Errorf("Check() = %v, want %v (subRequestMode=%v, urlPath=%q, X-Forwarded-Uri=%q)",
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xForwardedUri)
}
})
}
}
+2 -8
View File
@@ -96,29 +96,23 @@ func (hmc *HeaderMatchesChecker) Hash() string {
type PathChecker struct { type PathChecker struct {
regexp *regexp.Regexp regexp *regexp.Regexp
hash string hash string
subRequestMode bool
} }
func NewPathChecker(rexStr string, subrequestMode bool) (checker.Impl, error) { func NewPathChecker(rexStr string) (checker.Impl, error) {
rex, err := regexp.Compile(strings.TrimSpace(rexStr)) rex, err := regexp.Compile(strings.TrimSpace(rexStr))
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err) return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
} }
return &PathChecker{rex, internal.FastHash(rexStr), subrequestMode}, nil return &PathChecker{rex, internal.FastHash(rexStr)}, nil
} }
func (pc *PathChecker) Check(r *http.Request) (bool, error) { func (pc *PathChecker) Check(r *http.Request) (bool, error) {
if pc.subRequestMode {
originalUrl := r.Header.Get("X-Original-URI") originalUrl := r.Header.Get("X-Original-URI")
if originalUrl == "" {
originalUrl = r.Header.Get("X-Forwarded-Uri")
}
if originalUrl != "" { if originalUrl != "" {
if pc.regexp.MatchString(originalUrl) { if pc.regexp.MatchString(originalUrl) {
return true, nil return true, nil
} }
} }
}
if pc.regexp.MatchString(r.URL.Path) { if pc.regexp.MatchString(r.URL.Path) {
return true, nil return true, nil
+2 -223
View File
@@ -272,8 +272,8 @@ func TestPathChecker_XOriginalURI(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Create the PathChecker in subrequest mode so X-Original-URI is honored. // Create the PathChecker
pc, err := NewPathChecker(tt.regex, true) pc, err := NewPathChecker(tt.regex)
if err != nil { if err != nil {
if !tt.expectError { if !tt.expectError {
t.Fatalf("NewPathChecker() unexpected error: %v", err) t.Fatalf("NewPathChecker() unexpected error: %v", err)
@@ -305,224 +305,3 @@ func TestPathChecker_XOriginalURI(t *testing.T) {
}) })
} }
} }
// TestPathChecker_GHSA_6wcg_mqvh_fcvg is a regression test for
// https://github.com/TecharoHQ/anubis/security/advisories/GHSA-6wcg-mqvh-fcvg.
//
// PR https://github.com/TecharoHQ/anubis/pull/1015 added the ability for
// reverse proxies using Anubis in subrequest auth mode to look at the path
// of a request as there are many rules in the wild that rely on checking
// the path. This is how access to things like robots.txt or anything in the
// .well-known directory is unaffected by Anubis.
//
// However this logic was also enabled for non-subrequest deployments of Anubis,
// meaning that a specially crafted request could include a /.well-known/
// path in it and then get around Anubis with little effort.
//
// This fix gates the logic behind a new plumbed variable named subrequestMode
// that only fires when Anubis is running in subrequest auth mode. This
// properly contains that workaround so that the logic does not fire in
// most deployments.
func TestPathChecker_GHSA_6wcg_mqvh_fcvg(t *testing.T) {
tests := []struct {
name string
regex string
urlPath string
xOriginalURI string
subRequestMode bool
want bool
}{
{
name: "default mode ignores spoofed X-Original-URI when real path matches",
regex: "^/admin/.*",
urlPath: "/admin/secret",
xOriginalURI: "/public/index",
subRequestMode: false,
want: true,
},
{
name: "default mode ignores spoofed X-Original-URI when real path does not match",
regex: "^/admin/.*",
urlPath: "/public/index",
xOriginalURI: "/admin/secret",
subRequestMode: false,
want: false,
},
{
name: "default mode without X-Original-URI matches real path",
regex: "^/admin/.*",
urlPath: "/admin/dashboard",
xOriginalURI: "",
subRequestMode: false,
want: true,
},
{
name: "subrequest mode honors X-Original-URI",
regex: "^/admin/.*",
urlPath: "/auth",
xOriginalURI: "/admin/secret",
subRequestMode: true,
want: true,
},
{
name: "subrequest mode falls back to URL.Path when X-Original-URI does not match",
regex: "^/admin/.*",
urlPath: "/admin/dashboard",
xOriginalURI: "/public/index",
subRequestMode: true,
want: true,
},
{
name: "subrequest mode with empty X-Original-URI uses URL.Path",
regex: "^/admin/.*",
urlPath: "/admin/dashboard",
xOriginalURI: "",
subRequestMode: true,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pc, err := NewPathChecker(tt.regex, tt.subRequestMode)
if err != nil {
t.Fatalf("NewPathChecker(%q, %v) returned error: %v", tt.regex, tt.subRequestMode, err)
}
req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.urlPath, nil)
if err != nil {
t.Fatalf("http.NewRequest: %v", err)
}
if tt.xOriginalURI != "" {
req.Header.Set("X-Original-URI", tt.xOriginalURI)
}
got, err := pc.Check(req)
if err != nil {
t.Fatalf("Check() unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("Check() = %v, want %v (subRequestMode=%v, urlPath=%q, X-Original-URI=%q)",
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xOriginalURI)
}
})
}
}
func TestPathChecker_XForwardedUri(t *testing.T) {
tests := []struct {
name string
regex string
xForwardedUri string
xOriginalURI string
urlPath string
subRequestMode bool
want bool
}{
{
name: "X-Forwarded-Uri matches regex in subrequest mode",
regex: "^/admin/.*",
xForwardedUri: "/admin/users",
urlPath: "/.within.website/x/cmd/anubis/api/check",
subRequestMode: true,
want: true,
},
{
name: "X-Forwarded-Uri with query string",
regex: "^/admin/.*",
xForwardedUri: "/admin/users?page=1",
urlPath: "/.within.website/x/cmd/anubis/api/check",
subRequestMode: true,
want: true,
},
{
name: "X-Original-URI takes priority over X-Forwarded-Uri",
regex: "^/admin/.*",
xForwardedUri: "/public/page",
xOriginalURI: "/admin/users",
urlPath: "/.within.website/x/cmd/anubis/api/check",
subRequestMode: true,
want: true,
},
{
name: "falls back to X-Forwarded-Uri when no X-Original-URI",
regex: "^/admin/.*",
xForwardedUri: "/admin/dashboard",
urlPath: "/.within.website/x/cmd/anubis/api/check",
subRequestMode: true,
want: true,
},
{
name: "neither header matches, url path matches",
regex: "^/public/.*",
xForwardedUri: "/admin/users",
urlPath: "/public/page",
subRequestMode: true,
want: true,
},
{
name: "nothing matches",
regex: "^/admin/.*",
xForwardedUri: "/public/page",
urlPath: "/.within.website/x/cmd/anubis/api/check",
subRequestMode: true,
want: false,
},
{
name: "non-subrequest mode ignores X-Forwarded-Uri",
regex: "^/admin/.*",
xForwardedUri: "/admin/users",
urlPath: "/public/page",
subRequestMode: false,
want: false,
},
{
name: "non-subrequest mode uses url path",
regex: "^/admin/.*",
xForwardedUri: "/public/page",
urlPath: "/admin/secret",
subRequestMode: false,
want: true,
},
{
name: "empty X-Forwarded-Uri falls back to url path",
regex: "^/check$",
urlPath: "/check",
subRequestMode: true,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pc, err := NewPathChecker(tt.regex, tt.subRequestMode)
if err != nil {
t.Fatalf("NewPathChecker(%q, %v) returned error: %v", tt.regex, tt.subRequestMode, err)
}
req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.urlPath, nil)
if err != nil {
t.Fatalf("http.NewRequest: %v", err)
}
if tt.xForwardedUri != "" {
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
}
if tt.xOriginalURI != "" {
req.Header.Set("X-Original-URI", tt.xOriginalURI)
}
got, err := pc.Check(req)
if err != nil {
t.Fatalf("Check() unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("Check() = %v, want %v (subRequestMode=%v, urlPath=%q, X-Forwarded-Uri=%q, X-Original-URI=%q)",
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xForwardedUri, tt.xOriginalURI)
}
})
}
}
+4 -14
View File
@@ -27,7 +27,7 @@ var (
Applications = promauto.NewCounterVec(prometheus.CounterOpts{ Applications = promauto.NewCounterVec(prometheus.CounterOpts{
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", "asn", "asn_description"}) }, []string{"rule", "action"})
ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid") ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid")
warnedAboutThresholds = &atomic.Bool{} warnedAboutThresholds = &atomic.Bool{}
@@ -47,8 +47,6 @@ type ParsedConfig struct {
Dns *dns.Dns Dns *dns.Dns
Logger *slog.Logger Logger *slog.Logger
Metrics *config.Metrics Metrics *config.Metrics
ThothClient *thoth.Client
LogASN bool
} }
func newParsedConfig(orig *config.Config) *ParsedConfig { func newParsedConfig(orig *config.Config) *ParsedConfig {
@@ -60,7 +58,7 @@ func newParsedConfig(orig *config.Config) *ParsedConfig {
} }
} }
func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDifficulty int, logLevel string, subrequestMode bool) (*ParsedConfig, error) { func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDifficulty int, logLevel string) (*ParsedConfig, error) {
c, err := config.Load(fin, fname) c, err := config.Load(fin, fname)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -72,10 +70,6 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
result := newParsedConfig(c) result := newParsedConfig(c)
result.DefaultDifficulty = defaultDifficulty result.DefaultDifficulty = defaultDifficulty
result.LogASN = c.Logging.LogASN
if hasThothClient {
result.ThothClient = tc
}
if c.Logging.Level != nil { if c.Logging.Level != nil {
logLevel = c.Logging.Level.String() logLevel = c.Logging.Level.String()
@@ -100,10 +94,6 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
lg := result.Logger.With("at", "config-validate") lg := result.Logger.With("at", "config-validate")
if result.LogASN && !hasThothClient {
lg.Warn("logging.asn is enabled but no Thoth client is configured; ASN logging and metrics will be skipped. Please read https://anubis.techaro.lol/docs/admin/thoth for more information")
}
stFac, ok := store.Get(c.Store.Backend) stFac, ok := store.Get(c.Store.Backend)
switch ok { switch ok {
case true: case true:
@@ -152,7 +142,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
} }
if b.PathRegex != nil { if b.PathRegex != nil {
c, err := NewPathChecker(*b.PathRegex, subrequestMode) c, err := NewPathChecker(*b.PathRegex)
if err != nil { if err != nil {
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s path regex: %w", b.Name, err)) validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s path regex: %w", b.Name, err))
} else { } else {
@@ -170,7 +160,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
} }
if b.Expression != nil { if b.Expression != nil {
c, err := NewCELChecker(b.Expression, result.Dns, subrequestMode) c, err := NewCELChecker(b.Expression, result.Dns)
if err != nil { if err != nil {
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err)) validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
} else { } else {
+4 -4
View File
@@ -19,7 +19,7 @@ func TestDefaultPolicyMustParse(t *testing.T) {
} }
defer fin.Close() defer fin.Close()
if _, err := ParseConfig(ctx, fin, "botPolicies.yaml", anubis.DefaultDifficulty, "info", false); err != nil { if _, err := ParseConfig(ctx, fin, "botPolicies.yaml", anubis.DefaultDifficulty, "info"); err != nil {
t.Fatalf("can't parse config: %v", err) t.Fatalf("can't parse config: %v", err)
} }
} }
@@ -41,7 +41,7 @@ func TestGoodConfigs(t *testing.T) {
defer fin.Close() defer fin.Close()
ctx := thothmock.WithMockThoth(t) ctx := thothmock.WithMockThoth(t)
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err != nil { if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
}) })
@@ -53,7 +53,7 @@ func TestGoodConfigs(t *testing.T) {
} }
defer fin.Close() defer fin.Close()
if _, err := ParseConfig(t.Context(), fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err != nil { if _, err := ParseConfig(t.Context(), fin, fin.Name(), anubis.DefaultDifficulty, "info"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
}) })
@@ -77,7 +77,7 @@ func TestBadConfigs(t *testing.T) {
} }
defer fin.Close() defer fin.Close()
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err == nil { if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info"); err == nil {
t.Fatal(err) t.Fatal(err)
} else { } else {
t.Log(err) t.Log(err)
+1 -1
View File
@@ -18,7 +18,7 @@ func (c *Client) GeoIPCheckerFor(countries []string) checker.Impl {
var sb strings.Builder var sb strings.Builder
fmt.Fprintln(&sb, "GeoIPChecker") fmt.Fprintln(&sb, "GeoIPChecker")
for _, cc := range countries { for _, cc := range countries {
countryMap[strings.ToLower(cc)] = struct{}{} countryMap[cc] = struct{}{}
fmt.Fprintln(&sb, cc) fmt.Fprintln(&sb, cc)
} }
+537 -622
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -20,11 +20,11 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^21.0.1", "@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^21.0.1", "@commitlint/config-conventional": "^20.5.0",
"baseline-browser-mapping": "^2.10.30", "baseline-browser-mapping": "^2.10.15",
"cssnano": "^8.0.1", "cssnano": "^7.1.4",
"cssnano-preset-advanced": "^8.0.1", "cssnano-preset-advanced": "^7.0.12",
"esbuild": "^0.28.0", "esbuild": "^0.28.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"playwright": "^1.52.0", "playwright": "^1.52.0",
@@ -32,11 +32,11 @@
"postcss-import": "^16.1.1", "postcss-import": "^16.1.1",
"postcss-import-url": "^7.2.0", "postcss-import-url": "^7.2.0",
"postcss-url": "^10.1.3", "postcss-url": "^10.1.3",
"prettier": "^3.8.3" "prettier": "^3.8.1"
}, },
"dependencies": { "dependencies": {
"@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/sha256-js": "^5.2.0",
"preact": "^10.29.2" "preact": "^10.29.1"
}, },
"commitlint": { "commitlint": {
"extends": [ "extends": [
-16
View File
@@ -1,16 +0,0 @@
bots:
- name: block-admin-via-regex
path_regex: ^/admin(/.*)?$
action: DENY
- name: block-secret-via-cel
expression:
all:
- 'path.startsWith("/api/secret")'
action: DENY
- import: (data)/meta/default-config.yaml
status_codes:
CHALLENGE: 200
DENY: 403
-27
View File
@@ -1,27 +0,0 @@
services:
traefik:
image: traefik:v3.3
restart: always
ports:
- 8080:80
volumes:
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./http.yaml:/config/http.yaml:ro
anubis:
image: ko.local/anubis
restart: always
environment:
BIND: ":8080"
TARGET: " "
POLICY_FNAME: /etc/techaro/anubis.yaml
PUBLIC_URL: http://localhost:8080/.within.website/x/cmd/anubis
COOKIE_DOMAIN: localhost
USE_REMOTE_ADDRESS: "true"
volumes:
- ./anubis.yaml:/etc/techaro/anubis.yaml
backend:
image: ghcr.io/xe/x/httpdebug
pull_policy: always
restart: always
-30
View File
@@ -1,30 +0,0 @@
http:
middlewares:
anubis:
forwardAuth:
address: http://anubis:8080/.within.website/x/cmd/anubis/api/check
trustForwardHeader: true
routers:
anubis-assets:
rule: Host(`localhost`) && PathPrefix(`/.within.website/x/cmd/anubis`)
entryPoints:
- web
service: anubis
backend:
rule: Host(`localhost`)
entryPoints:
- web
service: backend
middlewares:
- anubis
services:
anubis:
loadBalancer:
servers:
- url: http://anubis:8080
backend:
loadBalancer:
servers:
- url: http://backend:3000
-33
View File
@@ -1,33 +0,0 @@
// Smoke test for https://github.com/TecharoHQ/anubis/issues/1628
//
// Traefik's forwardAuth middleware calls Anubis at the literal path
// /.within.website/x/cmd/anubis/api/check and conveys the original URL in the
// X-Forwarded-Uri header. Path-targeting policy rules must match that header
// (not r.URL.Path), otherwise every request looks like a request to /check.
const BASE = "http://localhost:8080";
const UA = "Mozilla/5.0 (compatible; AnubisTraefikSmoke/1.0)";
const cases = [
{ path: "/", expected: 307, why: "control: no DENY rule, default challenge redirect" },
{ path: "/free", expected: 307, why: "control: no DENY rule, default challenge redirect" },
{ path: "/admin", expected: 403, why: "path_regex must match X-Forwarded-Uri, not 307 or 200" },
{ path: "/admin/users", expected: 403, why: "path_regex must match X-Forwarded-Uri, not 307 or 200" },
{ path: "/api/secret", expected: 403, why: "CEL path must match X-Forwarded-Uri, not 307 or 200" },
];
let failed = false;
for (const c of cases) {
const resp = await fetch(`${BASE}${c.path}`, {
headers: { "User-Agent": UA },
redirect: "manual",
});
const ok = resp.status === c.expected;
console.log(
`${ok ? "PASS" : "FAIL"}: GET ${c.path}${resp.status} (want ${c.expected}: ${c.why})`,
);
if (!ok) failed = true;
}
process.exit(failed ? 1 : 0);
-22
View File
@@ -1,22 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
export VERSION=${GITHUB_SHA:-devel}-test
export KO_DOCKER_REPO=ko.local
set -u
source ../lib/lib.sh
build_anubis_ko
function cleanup() {
docker compose down -t 1 || :
}
trap cleanup EXIT SIGINT
docker compose up -d
backoff-retry --try-count 20 node ./test.mjs
-8
View File
@@ -1,8 +0,0 @@
entryPoints:
web:
address: ":80"
providers:
file:
directory: /config
watch: false
-2
View File
@@ -1,2 +0,0 @@
*
!.gitignore
+4 -46
View File
@@ -7,43 +7,17 @@ import (
"github.com/a-h/templ" "github.com/a-h/templ"
"github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/lib/challenge" "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/config" "github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
) )
// Options carries per-server render state for Anubis pages. Embedders can
// render multiple Server instances in one process without mutating package
// globals.
type Options struct {
BasePrefix string
PublicURL string
}
// DefaultOptions preserves the legacy package-global behavior for callers that
// render web components directly.
func DefaultOptions() Options {
return Options{
BasePrefix: anubis.BasePrefix,
PublicURL: anubis.PublicUrl,
}
}
func Base(title string, body templ.Component, impressum *config.Impressum, localizer *localization.SimpleLocalizer) templ.Component { func Base(title string, body templ.Component, impressum *config.Impressum, localizer *localization.SimpleLocalizer) templ.Component {
return BaseWithOptions(DefaultOptions(), title, body, impressum, localizer) return base(title, body, impressum, nil, nil, localizer)
}
func BaseWithOptions(opts Options, title string, body templ.Component, impressum *config.Impressum, localizer *localization.SimpleLocalizer) templ.Component {
return base(opts, title, body, impressum, nil, nil, localizer)
} }
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 { 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 BaseWithChallengeAndOGTagsWithOptions(DefaultOptions(), title, body, impressum, challenge, rules, ogTags, localizer) return base(title, body, impressum, struct {
}
func BaseWithChallengeAndOGTagsWithOptions(opts Options, 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(opts, title, body, impressum, struct {
Rules *config.ChallengeRules `json:"rules"` Rules *config.ChallengeRules `json:"rules"`
Challenge any `json:"challenge"` Challenge any `json:"challenge"`
}{ }{
@@ -53,27 +27,11 @@ func BaseWithChallengeAndOGTagsWithOptions(opts Options, title string, body temp
} }
func ErrorPage(msg, mail, code string, localizer *localization.SimpleLocalizer) templ.Component { func ErrorPage(msg, mail, code string, localizer *localization.SimpleLocalizer) templ.Component {
return ErrorPageWithOptions(DefaultOptions(), msg, mail, code, localizer) return errorPage(msg, mail, code, localizer)
}
func ErrorPageWithOptions(opts Options, msg, mail, code string, localizer *localization.SimpleLocalizer) templ.Component {
return errorPage(opts, msg, mail, code, localizer)
}
func StaticHappy(localizer *localization.SimpleLocalizer) templ.Component {
return StaticHappyWithOptions(DefaultOptions(), localizer)
}
func StaticHappyWithOptions(opts Options, localizer *localization.SimpleLocalizer) templ.Component {
return staticHappy(opts, localizer)
} }
func Bench(localizer *localization.SimpleLocalizer) templ.Component { func Bench(localizer *localization.SimpleLocalizer) templ.Component {
return BenchWithOptions(DefaultOptions(), localizer) return bench(localizer)
}
func BenchWithOptions(opts Options, localizer *localization.SimpleLocalizer) templ.Component {
return bench(opts, localizer)
} }
func honeypotLink(href string) templ.Component { func honeypotLink(href string) templ.Component {
+13 -13
View File
@@ -9,12 +9,12 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
templ base(opts Options, title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) { templ base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) {
<!DOCTYPE html> <!DOCTYPE html>
<html lang={ localizer.GetLang() }> <html lang={ localizer.GetLang() }>
<head> <head>
<title>{ title }</title> <title>{ title }</title>
<link rel="stylesheet" href={ opts.BasePrefix + xess.URL }/> <link rel="stylesheet" href={ anubis.BasePrefix + xess.URL }/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="robots" content="noindex,nofollow"/> <meta name="robots" content="noindex,nofollow"/>
for key, value := range ogTags { for key, value := range ogTags {
@@ -60,11 +60,11 @@ templ base(opts Options, title string, body templ.Component, impressum *config.I
</style> </style>
@templ.JSONScript("anubis_version", anubis.Version) @templ.JSONScript("anubis_version", anubis.Version)
@templ.JSONScript("anubis_challenge", challenge) @templ.JSONScript("anubis_challenge", challenge)
@templ.JSONScript("anubis_base_prefix", opts.BasePrefix) @templ.JSONScript("anubis_base_prefix", anubis.BasePrefix)
@templ.JSONScript("anubis_public_url", opts.PublicURL) @templ.JSONScript("anubis_public_url", anubis.PublicUrl)
</head> </head>
<body id="top"> <body id="top">
@honeypotLink(opts.BasePrefix + fmt.Sprintf("%shoneypot/%s/init", anubis.APIPrefix, uuid.NewString())) @honeypotLink(anubis.BasePrefix + fmt.Sprintf("%shoneypot/%s/init", anubis.APIPrefix, uuid.NewString()))
<main> <main>
<h1 id="title" class="centered-div">{ title }</h1> <h1 id="title" class="centered-div">{ title }</h1>
@body @body
@@ -79,7 +79,7 @@ templ base(opts Options, title string, body templ.Component, impressum *config.I
if impressum != nil { if impressum != nil {
<p> <p>
@templ.Raw(impressum.Footer) @templ.Raw(impressum.Footer)
-- <a href={ templ.SafeURL(opts.BasePrefix + fmt.Sprintf("%simprint", anubis.APIPrefix)) }>Imprint</a> -- <a href={ templ.SafeURL(anubis.BasePrefix + fmt.Sprintf("%simprint", anubis.APIPrefix)) }>Imprint</a>
</p> </p>
} }
<p>{ localizer.T("version_info") } <code>{ anubis.Version }</code>.</p> <p>{ localizer.T("version_info") } <code>{ anubis.Version }</code>.</p>
@@ -90,9 +90,9 @@ templ base(opts Options, title string, body templ.Component, impressum *config.I
</html> </html>
} }
templ errorPage(opts Options, message, mail, code string, localizer *localization.SimpleLocalizer) { templ errorPage(message, mail, code string, localizer *localization.SimpleLocalizer) {
<div class="centered-div"> <div class="centered-div">
<img id="image" alt="Sad Anubis" style="width:100%;max-width:256px;" src={ opts.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version }/> <img id="image" alt="Sad Anubis" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version }/>
<p>{ message }.</p> <p>{ message }.</p>
if code != "" { if code != "" {
<code><pre>{ code }</pre></code> <code><pre>{ code }</pre></code>
@@ -110,19 +110,19 @@ templ errorPage(opts Options, message, mail, code string, localizer *localizatio
</div> </div>
} }
templ staticHappy(opts Options, localizer *localization.SimpleLocalizer) { templ StaticHappy(localizer *localization.SimpleLocalizer) {
<div class="centered-div"> <div class="centered-div">
<img <img
style="display:none;" style="display:none;"
style="width:100%;max-width:256px;" style="width:100%;max-width:256px;"
src={ opts.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + src={ "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
anubis.Version } anubis.Version }
/> />
<p>{ localizer.T("static_check_endpoint") }</p> <p>{ localizer.T("static_check_endpoint") }</p>
</div> </div>
} }
templ bench(opts Options, localizer *localization.SimpleLocalizer) { templ bench(localizer *localization.SimpleLocalizer) {
<div style="height:20rem;display:flex"> <div style="height:20rem;display:flex">
<table style="margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem"> <table style="margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem">
<thead <thead
@@ -145,9 +145,9 @@ templ bench(opts Options, localizer *localization.SimpleLocalizer) {
></tbody> ></tbody>
</table> </table>
<div class="centered-div"> <div class="centered-div">
<img id="image" style="width:100%;max-width:256px;" src={ opts.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/> <img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/>
<p id="status" style="max-width:256px">{ localizer.T("loading") }</p> <p id="status" style="max-width:256px">{ localizer.T("loading") }</p>
<script async type="module" src={ opts.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version }></script> <script async type="module" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version }></script>
<div id="sparkline"></div> <div id="sparkline"></div>
<noscript> <noscript>
<p>{ localizer.T("benchmark_requires_js") }</p> <p>{ localizer.T("benchmark_requires_js") }</p>
+18 -18
View File
@@ -17,7 +17,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
func base(opts Options, title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) templ.Component { func base(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -69,9 +69,9 @@ func base(opts Options, title string, body templ.Component, impressum *config.Im
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 templ.SafeURL var templ_7745c5c3_Var4 templ.SafeURL
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(opts.BasePrefix + xess.URL) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(anubis.BasePrefix + xess.URL)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 17, Col: 59} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 17, Col: 61}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -125,11 +125,11 @@ func base(opts Options, title string, body templ.Component, impressum *config.Im
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templ.JSONScript("anubis_base_prefix", opts.BasePrefix).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = templ.JSONScript("anubis_base_prefix", anubis.BasePrefix).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templ.JSONScript("anubis_public_url", opts.PublicURL).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = templ.JSONScript("anubis_public_url", anubis.PublicUrl).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -137,7 +137,7 @@ func base(opts Options, title string, body templ.Component, impressum *config.Im
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = honeypotLink(opts.BasePrefix+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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -245,9 +245,9 @@ func base(opts Options, title string, body templ.Component, impressum *config.Im
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var13 templ.SafeURL var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(opts.BasePrefix + 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 { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 82, Col: 96} 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -292,7 +292,7 @@ func base(opts Options, title string, body templ.Component, impressum *config.Im
}) })
} }
func errorPage(opts Options, message, mail, code string, localizer *localization.SimpleLocalizer) templ.Component { func errorPage(message, mail, code string, localizer *localization.SimpleLocalizer) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -318,9 +318,9 @@ func errorPage(opts Options, message, mail, code string, localizer *localization
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(opts.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 95, Col: 179} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 95, Col: 181}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -446,7 +446,7 @@ func errorPage(opts Options, message, mail, code string, localizer *localization
}) })
} }
func staticHappy(opts Options, localizer *localization.SimpleLocalizer) templ.Component { func StaticHappy(localizer *localization.SimpleLocalizer) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -472,7 +472,7 @@ func staticHappy(opts Options, localizer *localization.SimpleLocalizer) templ.Co
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var26 string var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(opts.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
anubis.Version) anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 119, Col: 18} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 119, Col: 18}
@@ -502,7 +502,7 @@ func staticHappy(opts Options, localizer *localization.SimpleLocalizer) templ.Co
}) })
} }
func bench(opts Options, localizer *localization.SimpleLocalizer) templ.Component { func bench(localizer *localization.SimpleLocalizer) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -606,9 +606,9 @@ func bench(opts Options, localizer *localization.SimpleLocalizer) templ.Componen
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var35 string var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(opts.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 148, Col: 164} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 148, Col: 166}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -632,9 +632,9 @@ func bench(opts Options, localizer *localization.SimpleLocalizer) templ.Componen
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var37 string var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(opts.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version) templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 150, Col: 136} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 150, Col: 138}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
+1 -1
View File
@@ -59,7 +59,7 @@ func TestBasePrefixInLinks(t *testing.T) {
// Render the base template to a buffer // Render the base template to a buffer
var buf strings.Builder var buf strings.Builder
component := base(Options{BasePrefix: tt.basePrefix}, tt.name, templ.NopComponent, impressum, nil, nil, localizer) component := base(tt.name, templ.NopComponent, impressum, nil, nil, localizer)
err := component.Render(context.Background(), &buf) err := component.Render(context.Background(), &buf)
if err != nil { if err != nil {
t.Fatalf("failed to render template: %v", err) t.Fatalf("failed to render template: %v", err)