Compare commits

..

3 Commits

Author SHA1 Message Date
Jason Cameron cca247c25b refactor(web): pass render URL state explicitly
Signed-off-by: Jason Cameron <jason.cameron@stanwith.me>
2026-05-14 01:33:27 -04:00
Jason Cameron c129cafc3a docs: update changelog for server URL state
Signed-off-by: Jason Cameron <jason.cameron@stanwith.me>
2026-05-14 01:20:38 -04:00
Jason Cameron b250fea00b refactor(lib): keep server URL state local
Signed-off-by: Jason Cameron <jason.cameron@stanwith.me>
2026-05-14 01:19:58 -04:00
22 changed files with 302 additions and 138 deletions
+1 -1
View File
@@ -14,7 +14,6 @@ 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. - 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.
- 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). - 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)
@@ -29,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix a bug in the dataset poisoning maze that could allow denial of service [#1580](https://github.com/TecharoHQ/anubis/issues/1580). - 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. - Add config option to add ASN to logs/metrics.
- Log weight when issuing challenge - Log weight when issuing challenge
- Keep Anubis server URL state local to each `lib.Server` instance to make embedded use safer.
## v1.25.0: Necron ## v1.25.0: Necron
+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=
-4
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"
@@ -169,9 +168,6 @@ func (i *Impl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
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()
+42 -15
View File
@@ -33,6 +33,7 @@ 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" iptoasnv1 "github.com/TecharoHQ/thoth-proto/gen/techaro/thoth/iptoasn/v1"
// challenge implementations // challenge implementations
@@ -97,6 +98,8 @@ 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
} }
@@ -123,6 +126,42 @@ func (s *Server) getRequestLogger(r *http.Request) (*slog.Logger, *http.Request)
return lg, r 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 {
@@ -246,11 +285,7 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
return return
} }
// Adjust cookie path if base prefix is not empty cookiePath := s.cookiePath()
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 {
@@ -344,11 +379,7 @@ 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 {
// Adjust cookie path if base prefix is not empty cookiePath := s.cookiePath()
cookiePath := "/"
if anubis.BasePrefix != "" {
cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/"
}
localizer := localization.GetLocalizer(r) localizer := localization.GetLocalizer(r)
@@ -511,11 +542,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
return return
} }
// Adjust cookie path if base prefix is not empty cookiePath := s.cookiePath()
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})
+92 -2
View File
@@ -2,6 +2,7 @@ package lib
import ( import (
"bytes" "bytes"
"compress/gzip"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -501,8 +502,11 @@ 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) {
// Reset the global BasePrefix before each test originalBasePrefix := anubis.BasePrefix
anubis.BasePrefix = "" anubis.BasePrefix = "/not-this-server"
t.Cleanup(func() {
anubis.BasePrefix = originalBasePrefix
})
pol := loadPolicies(t, "", 4) pol := loadPolicies(t, "", 4)
@@ -631,10 +635,96 @@ 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,6 +44,7 @@ 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
+2 -3
View File
@@ -7,7 +7,6 @@ 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"
@@ -28,7 +27,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(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge") u, err := r.URL.Parse(in.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)
} }
@@ -47,7 +46,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(u.String(), in.Rule.Challenge.Difficulty, showMeta, loc) result := page(in.BasePrefix, 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(redir string, difficulty int, showMeta bool, loc *localization.SimpleLocalizer) { templ page(basePrefix, 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={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/> <img id="image" style="width:100%;max-width:256px;" src={ basePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.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 }/> <img style="display:none;" style="width:100%;max-width:256px;" src={ 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(redir string, difficulty int, showMeta bool, loc *localization.SimpleLocalizer) templ.Component { func page(basePrefix, 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(redir string, difficulty int, showMeta bool, loc *localization.SimpleL
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(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(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: 165} return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 12, Col: 158}
} }
_, 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(redir string, difficulty int, showMeta bool, loc *localization.SimpleL
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(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(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: 174} return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 13, Col: 167}
} }
_, 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 {
+2 -3
View File
@@ -10,7 +10,6 @@ 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"
@@ -43,7 +42,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(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge") u, err := r.URL.Parse(in.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)
} }
@@ -55,7 +54,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(u.String(), in.Challenge.RandomData, in.Rule.Challenge.Difficulty, loc) result := page(in.BasePrefix, 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(redir, challenge string, difficulty int, loc *localization.SimpleLocalizer) { templ page(basePrefix, 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={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/> <img id="image" style="width:100%;max-width:256px;" src={ 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(redir, challenge string, difficulty int, loc *localization.SimpleLoca
"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": anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version, "pensive_url": 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(redir, challenge string, difficulty int, loc *localization.SimpleLocalizer) templ.Component { func page(basePrefix, 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(redir, challenge string, difficulty int, loc *localization.SimpleLocal
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(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(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: 166} return templ.Error{Err: templ_7745c5c3_Err, FileName: `preact.templ`, Line: 11, Col: 159}
} }
_, 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(redir, challenge string, difficulty int, loc *localization.SimpleLocal
"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": anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version, "pensive_url": 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(loc), nil return page(in.BasePrefix, 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(localizer *localization.SimpleLocalizer) { templ page(basePrefix string, localizer *localization.SimpleLocalizer) {
<div class="centered-div"> <div class="centered-div">
<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 id="image" style="width:100%;max-width:256px;" src={ basePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.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 }/> <img style="display:none;" style="width:100%;max-width:256px;" src={ 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={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script> <script async type="module" src={ 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(localizer *localization.SimpleLocalizer) templ.Component { func page(basePrefix 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 {
@@ -39,9 +39,9 @@ func page(localizer *localization.SimpleLocalizer) templ.Component {
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(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(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: 165} return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 10, Col: 158}
} }
_, 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(localizer *localization.SimpleLocalizer) templ.Component {
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(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(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: 174} return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 11, Col: 167}
} }
_, 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(localizer *localization.SimpleLocalizer) templ.Component {
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(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(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: 136} return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 13, Col: 129}
} }
_, 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 {
+12 -18
View File
@@ -112,8 +112,7 @@ func New(opts Options) (*Server, error) {
opts.ED25519PrivateKey = priv opts.ED25519PrivateKey = priv
} }
anubis.BasePrefix = strings.TrimRight(opts.BasePrefix, "/") opts.BasePrefix = strings.TrimRight(opts.BasePrefix, "/")
anubis.PublicUrl = opts.PublicUrl
result := &Server{ result := &Server{
next: opts.Next, next: opts.Next,
@@ -121,6 +120,8 @@ 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,
@@ -131,28 +132,20 @@ func New(opts Options) (*Server, error) {
} }
mux := http.NewServeMux() mux := http.NewServeMux()
xess.Mount(mux) xessPrefix := result.prefixedPath(xess.BasePrefix)
mux.Handle(xessPrefix, internal.UnchangingCache(http.StripPrefix(xessPrefix, http.FileServerFS(xess.Static))))
// Helper to add global prefix // Helper to add the server-local base 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
} }
// Ensure there's no double slash when concatenating BasePrefix and pattern mux.Handle(method+result.prefixedPath(pattern), handler)
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 := strings.TrimSuffix(anubis.BasePrefix, "/") + anubis.StaticPath stripPrefix := result.prefixedPath(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 {
@@ -166,9 +159,10 @@ 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) {
templ.Handler( handler := templ.Handler(
web.Base(opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum, localization.GetLocalizer(r)), web.BaseWithOptions(result.renderOptions(), opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum, localization.GetLocalizer(r)),
).ServeHTTP(w, r) )
handler.ServeHTTP(w, r)
}), "GET") }), "GET")
} }
+32 -16
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.opts.PublicUrl == "" { if s.configuredPublicURL() == "" {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(localizer.T("authorization_required"))) w.Write([]byte(localizer.T("authorization_required")))
} else { } else {
@@ -263,6 +263,7 @@ 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,
@@ -277,7 +278,8 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
return return
} }
page := web.BaseWithChallengeAndOGTags( page := web.BaseWithChallengeAndOGTagsWithOptions(
s.renderOptions(),
localizer.T("making_sure_not_bot"), localizer.T("making_sure_not_bot"),
component, component,
s.policy.Impressum, s.policy.Impressum,
@@ -323,15 +325,17 @@ func (s *Server) constructRedirectURL(r *http.Request) (string, error) {
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.opts.PublicUrl, escapedURL), nil return fmt.Sprintf("%s/.within.website/?redir=%s", s.configuredPublicURL(), 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()
templ.Handler( handler := templ.Handler(
web.Base(localizer.T("benchmarking_anubis"), web.Bench(localizer), s.policy.Impressum, localizer), web.BaseWithOptions(opts, localizer.T("benchmarking_anubis"), web.BenchWithOptions(opts, 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) {
@@ -340,10 +344,12 @@ 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.Base( component := web.BaseWithOptions(
opts,
localizer.T("oh_noes"), localizer.T("oh_noes"),
web.ErrorPage(msg, s.opts.WebmasterEmail, code, localizer), web.ErrorPageWithOptions(opts, msg, s.opts.WebmasterEmail, code, localizer),
s.policy.Impressum, s.policy.Impressum,
localizer, localizer,
) )
@@ -352,17 +358,25 @@ 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, anubis.BasePrefix+anubis.StaticPath) { if strings.HasPrefix(r.URL.Path, s.prefixedPath(anubis.StaticPath)) {
s.mux.ServeHTTP(w, r) s.mux.ServeHTTP(w, r)
return return
} else if strings.HasPrefix(r.URL.Path, anubis.BasePrefix+xess.BasePrefix) { } else if strings.HasPrefix(r.URL.Path, s.prefixedPath(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 := strings.TrimPrefix(r.URL.Path, anubis.BasePrefix) path := r.URL.Path
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
@@ -373,11 +387,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 {
if !s.opts.StripBasePrefix || s.opts.BasePrefix == "" { basePrefix := s.configuredBasePrefix()
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) {
@@ -441,9 +455,11 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
return return
} }
templ.Handler( opts := s.renderOptions()
web.Base(localizer.T("you_are_not_a_bot"), web.StaticHappy(localizer), s.policy.Impressum, localizer), handler := templ.Handler(
).ServeHTTP(w, r) web.BaseWithOptions(opts, localizer.T("you_are_not_a_bot"), web.StaticHappyWithOptions(opts, localizer), s.policy.Impressum, localizer),
)
handler.ServeHTTP(w, r)
} else { } else {
asn, asnDesc := asnFromContext(r.Context()) asn, asnDesc := asnFromContext(r.Context())
requestsProxied.WithLabelValues(r.Host, asn, asnDesc).Inc() requestsProxied.WithLabelValues(r.Host, asn, asnDesc).Inc()
+46 -4
View File
@@ -7,17 +7,43 @@ 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 base(title, body, impressum, nil, nil, localizer) return BaseWithOptions(DefaultOptions(), title, body, impressum, 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 base(title, body, impressum, struct { return BaseWithChallengeAndOGTagsWithOptions(DefaultOptions(), title, body, impressum, challenge, rules, ogTags, localizer)
}
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"`
}{ }{
@@ -27,11 +53,27 @@ func BaseWithChallengeAndOGTags(title string, body templ.Component, impressum *c
} }
func ErrorPage(msg, mail, code string, localizer *localization.SimpleLocalizer) templ.Component { func ErrorPage(msg, mail, code string, localizer *localization.SimpleLocalizer) templ.Component {
return errorPage(msg, mail, code, localizer) return ErrorPageWithOptions(DefaultOptions(), 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 bench(localizer) return BenchWithOptions(DefaultOptions(), 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(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) { templ base(opts Options, 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={ anubis.BasePrefix + xess.URL }/> <link rel="stylesheet" href={ opts.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(title string, body templ.Component, impressum *config.Impressum, chal
</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", anubis.BasePrefix) @templ.JSONScript("anubis_base_prefix", opts.BasePrefix)
@templ.JSONScript("anubis_public_url", anubis.PublicUrl) @templ.JSONScript("anubis_public_url", opts.PublicURL)
</head> </head>
<body id="top"> <body id="top">
@honeypotLink(anubis.BasePrefix + fmt.Sprintf("%shoneypot/%s/init", anubis.APIPrefix, uuid.NewString())) @honeypotLink(opts.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(title string, body templ.Component, impressum *config.Impressum, chal
if impressum != nil { if impressum != nil {
<p> <p>
@templ.Raw(impressum.Footer) @templ.Raw(impressum.Footer)
-- <a href={ templ.SafeURL(anubis.BasePrefix + fmt.Sprintf("%simprint", anubis.APIPrefix)) }>Imprint</a> -- <a href={ templ.SafeURL(opts.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(title string, body templ.Component, impressum *config.Impressum, chal
</html> </html>
} }
templ errorPage(message, mail, code string, localizer *localization.SimpleLocalizer) { templ errorPage(opts Options, 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={ anubis.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={ opts.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(message, mail, code string, localizer *localization.SimpleLocali
</div> </div>
} }
templ StaticHappy(localizer *localization.SimpleLocalizer) { templ staticHappy(opts Options, 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={ "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + src={ opts.BasePrefix + "/.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(localizer *localization.SimpleLocalizer) { templ bench(opts Options, 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(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={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/> <img id="image" style="width:100%;max-width:256px;" src={ opts.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={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version }></script> <script async type="module" src={ opts.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(title string, body templ.Component, impressum *config.Impressum, challenge any, ogTags map[string]string, localizer *localization.SimpleLocalizer) templ.Component { func base(opts Options, 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(title string, body templ.Component, impressum *config.Impressum, chall
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(anubis.BasePrefix + xess.URL) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(opts.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: 61} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 17, Col: 59}
} }
_, 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(title string, body templ.Component, impressum *config.Impressum, chall
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", anubis.BasePrefix).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = templ.JSONScript("anubis_base_prefix", opts.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", anubis.PublicUrl).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = templ.JSONScript("anubis_public_url", opts.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(title string, body templ.Component, impressum *config.Impressum, chall
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = honeypotLink(anubis.BasePrefix+fmt.Sprintf("%shoneypot/%s/init", anubis.APIPrefix, uuid.NewString())).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = honeypotLink(opts.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(title string, body templ.Component, impressum *config.Impressum, chall
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(anubis.BasePrefix + fmt.Sprintf("%simprint", anubis.APIPrefix))) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(opts.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: 98} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 82, Col: 96}
} }
_, 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(title string, body templ.Component, impressum *config.Impressum, chall
}) })
} }
func errorPage(message, mail, code string, localizer *localization.SimpleLocalizer) templ.Component { func errorPage(opts Options, 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(message, mail, code string, localizer *localization.SimpleLocaliz
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(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/reject.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(opts.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: 181} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 95, Col: 179}
} }
_, 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(message, mail, code string, localizer *localization.SimpleLocaliz
}) })
} }
func StaticHappy(localizer *localization.SimpleLocalizer) templ.Component { func staticHappy(opts Options, 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(localizer *localization.SimpleLocalizer) templ.Component {
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("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(opts.BasePrefix + "/.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(localizer *localization.SimpleLocalizer) templ.Component {
}) })
} }
func bench(localizer *localization.SimpleLocalizer) templ.Component { func bench(opts Options, 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(localizer *localization.SimpleLocalizer) templ.Component {
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(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(opts.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: 166} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 148, Col: 164}
} }
_, 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(localizer *localization.SimpleLocalizer) templ.Component {
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(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version) templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(opts.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: 138} return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 150, Col: 136}
} }
_, 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(tt.name, templ.NopComponent, impressum, nil, nil, localizer) component := base(Options{BasePrefix: tt.basePrefix}, 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)