diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index 37bb248..0c16101 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -89,3 +89,5 @@ ^lib/policy/config/testdata/bad/unparseable\.json$ ignore$ robots.txt +^lib/localization/locales/.*\.json$ +^lib/localization/.*_test.go$ diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 8c8b2b0..2a3663d 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -33,7 +33,7 @@ Caddyfile caninetools Cardyb celchecker -CELPHASE +celphase cerr certresolver cespare @@ -42,6 +42,7 @@ cgr chainguard chall challengemozilla +Chargement checkpath checkresult chibi @@ -183,6 +184,7 @@ mozilla nbf netsurf nginx +nicksnyder nobots NONINFRINGEMENT nosleep diff --git a/.gitignore b/.gitignore index a716c66..0fc14c2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ node_modules # how does this get here doc/VERSION + +web/static/locales/*.json \ No newline at end of file diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 98f4612..b008a5c 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Implement localization system. Find locale files in lib/localization/locales/. + ## v1.20.0: Thancred Waters The big ticket items are as follows: diff --git a/go.mod b/go.mod index 0f879e9..774c311 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( cel.dev/expr v0.23.1 // indirect dario.cat/mergo v1.0.2 // indirect github.com/AlekSi/pointer v1.2.0 // indirect - github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/BurntSushi/toml v1.5.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect @@ -87,6 +87,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/natefinch/atomic v1.0.1 // indirect + github.com/nicksnyder/go-i18n/v2 v2.6.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -133,6 +134,7 @@ require ( tool ( github.com/TecharoHQ/yeet/cmd/yeet github.com/a-h/templ/cmd/templ + github.com/nicksnyder/go-i18n/v2/goi18n github.com/suzuki-shunsuke/pinact/cmd/pinact golang.org/x/tools/cmd/deadcode golang.org/x/tools/cmd/goimports diff --git a/go.sum b/go.sum index 609123c..77c31a5 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= @@ -237,6 +239,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= +github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= diff --git a/lib/anubis.go b/lib/anubis.go index 22c32d5..940be04 100644 --- a/lib/anubis.go +++ b/lib/anubis.go @@ -26,6 +26,7 @@ import ( "github.com/TecharoHQ/anubis/internal/dnsbl" "github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/lib/challenge" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy/checker" "github.com/TecharoHQ/anubis/lib/policy/config" @@ -87,6 +88,8 @@ func (s *Server) getTokenKeyfunc() jwt.Keyfunc { } } + + func (s *Server) challengeFor(r *http.Request, difficulty int) string { var fp [32]byte if len(s.hs512Secret) == 0 { @@ -126,7 +129,8 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS cr, rule, err := s.check(r) if err != nil { lg.Error("check failed", "err", err) - s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy\"") + localizer := localization.GetLocalizer(r) + s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy\"", localizer.T("internal_server_error"))) return } @@ -210,6 +214,8 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch cookiePath = strings.TrimSuffix(anubis.BasePrefix, "/") + "/" } + localizer := localization.GetLocalizer(r) + switch cr.Rule { case config.RuleAllow: lg.Debug("allowing traffic to origin (explicit)") @@ -220,13 +226,13 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch lg.Info("explicit deny") if rule == nil { lg.Error("rule is nil, cannot calculate checksum") - s.respondWithError(w, r, "Internal Server Error: Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.RuleDeny\"") + s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.RuleDeny\"", localizer.T("internal_server_error"))) return true } hash := rule.Hash() lg.Debug("rule hash", "hash", hash) - s.respondWithStatus(w, r, fmt.Sprintf("Access Denied: error code %s", hash), s.policy.StatusCodes.Deny) + s.respondWithStatus(w, r, fmt.Sprintf("%s %s", localizer.T("access_denied"), hash), s.policy.StatusCodes.Deny) return true case config.RuleChallenge: lg.Debug("challenge requested") @@ -237,7 +243,7 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch default: s.ClearCookie(w, s.cookieName, cookiePath, r.Host) slog.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule) - s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy.Rules\"") + s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.Rules\"", localizer.T("internal_server_error"))) return true } return false @@ -258,7 +264,12 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string, if resp != dnsbl.AllGood { lg.Info("DNSBL hit", "status", resp.String()) - s.respondWithStatus(w, r, fmt.Sprintf("DroneBL reported an entry: %s, see https://dronebl.org/lookup?ip=%s", resp.String(), ip), s.policy.StatusCodes.Deny) + localizer := localization.GetLocalizer(r) + s.respondWithStatus(w, r, fmt.Sprintf("%s: %s, %s https://dronebl.org/lookup?ip=%s", + localizer.T("dronebl_entry"), + resp.String(), + localizer.T("see_dronebl_lookup"), + ip), s.policy.StatusCodes.Deny) return true } } @@ -267,6 +278,7 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string, func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) { lg := internal.GetRequestLogger(r) + localizer := localization.GetLocalizer(r) redir := r.FormValue("redir") if redir == "" { @@ -276,7 +288,7 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) { encoder.Encode(struct { Error string `json:"error"` }{ - Error: "Invalid invocation of MakeChallenge", + Error: localizer.T("invalid_invocation"), }) return } @@ -291,7 +303,7 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) { err := encoder.Encode(struct { Error string `json:"error"` }{ - Error: "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"makeChallenge\"", + Error: fmt.Sprintf("%s \"makeChallenge\"", localizer.T("internal_server_error")), }) if err != nil { lg.Error("failed to encode error response", "err", err) @@ -322,6 +334,7 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) { func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { lg := internal.GetRequestLogger(r) + localizer := localization.GetLocalizer(r) // Adjust cookie path if base prefix is not empty cookiePath := "/" @@ -333,7 +346,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { s.ClearCookie(w, s.cookieName, cookiePath, r.Host) s.ClearCookie(w, anubis.TestCookieName, "/", r.Host) lg.Warn("user has cookies disabled, this is not an anubis bug") - s.respondWithError(w, r, "Your browser is configured to disable cookies. Anubis requires cookies for the legitimate interest of making sure you are a valid client. Please enable cookies for this domain") + s.respondWithError(w, r, localizer.T("cookies_disabled")) return } @@ -343,7 +356,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { redirURL, err := url.ParseRequestURI(redir) if err != nil { lg.Error("invalid redirect", "err", err) - s.respondWithError(w, r, "Invalid redirect") + s.respondWithError(w, r, localizer.T("invalid_redirect")) return } // used by the path checker rule @@ -351,18 +364,18 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { urlParsed, err := r.URL.Parse(redir) if err != nil { - s.respondWithError(w, r, "Redirect URL not parseable") + s.respondWithError(w, r, localizer.T("redirect_not_parseable")) return } if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host { - s.respondWithError(w, r, "Redirect domain not allowed") + s.respondWithError(w, r, localizer.T("redirect_domain_not_allowed")) return } cr, rule, err := s.check(r) if err != nil { lg.Error("check failed", "err", err) - s.respondWithError(w, r, "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\"") + s.respondWithError(w, r, fmt.Sprintf("%s \"passChallenge\"", localizer.T("internal_server_error"))) return } lg = lg.With("check_result", cr) @@ -370,7 +383,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { impl, ok := challenge.Get(rule.Challenge.Algorithm) if !ok { lg.Error("check failed", "err", err) - s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm)) + s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm)) return } @@ -403,7 +416,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { if err != nil { lg.Error("failed to sign JWT", "err", err) s.ClearCookie(w, s.cookieName, cookiePath, r.Host) - s.respondWithError(w, r, "failed to sign JWT") + s.respondWithError(w, r, localizer.T("failed_to_sign_jwt")) return } diff --git a/lib/challenge/metarefresh/metarefresh.go b/lib/challenge/metarefresh/metarefresh.go index 99afc28..68a2ed0 100644 --- a/lib/challenge/metarefresh/metarefresh.go +++ b/lib/challenge/metarefresh/metarefresh.go @@ -8,6 +8,7 @@ import ( "github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis/lib/challenge" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/web" "github.com/a-h/templ" @@ -34,7 +35,9 @@ func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *challenge.IssueInput) q.Set("challenge", in.Challenge) u.RawQuery = q.Encode() - component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", page(in.Challenge, u.String(), in.Rule.Challenge.Difficulty), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags) + loc := localization.GetLocalizer(r) + component, err := web.BaseWithChallengeAndOGTags(loc.T("making_sure_not_bot"), page(in.Challenge, u.String(), in.Rule.Challenge.Difficulty, loc), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags, loc) + if err != nil { return nil, fmt.Errorf("can't render page: %w", err) } diff --git a/lib/challenge/metarefresh/metarefresh.templ b/lib/challenge/metarefresh/metarefresh.templ index 1dba375..e4549b6 100644 --- a/lib/challenge/metarefresh/metarefresh.templ +++ b/lib/challenge/metarefresh/metarefresh.templ @@ -4,14 +4,15 @@ import ( "fmt" "github.com/TecharoHQ/anubis" + "github.com/TecharoHQ/anubis/lib/localization" ) -templ page(challenge, redir string, difficulty int) { +templ page(challenge, redir string, difficulty int, loc *localization.SimpleLocalizer) {
Loading...
-Please wait a moment while we ensure the security of your connection.
+{ loc.T("loading") }
+{ loc.T("connection_security") }
Loading...
Please wait a moment while we ensure the security of your connection.
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var4 string - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir)) + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(loc.T("loading")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 15, Col: 83} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 14, Col: 35} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(loc.T("connection_security")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 15, Col: 35} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/lib/challenge/proofofwork/proofofwork.go b/lib/challenge/proofofwork/proofofwork.go index 9f02ce7..74a3551 100644 --- a/lib/challenge/proofofwork/proofofwork.go +++ b/lib/challenge/proofofwork/proofofwork.go @@ -10,6 +10,7 @@ import ( "github.com/TecharoHQ/anubis/internal" chall "github.com/TecharoHQ/anubis/lib/challenge" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/web" "github.com/a-h/templ" @@ -29,7 +30,8 @@ func (i *Impl) Setup(mux *http.ServeMux) { } func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) { - component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags) + loc := localization.GetLocalizer(r) + component, err := web.BaseWithChallengeAndOGTags(loc.T("making_sure_not_bot"), web.Index(loc), in.Impressum, in.Challenge, in.Rule.Challenge, in.OGTags, loc) if err != nil { return nil, fmt.Errorf("can't render page: %w", err) } diff --git a/lib/config.go b/lib/config.go index 6c832aa..8041da8 100644 --- a/lib/config.go +++ b/lib/config.go @@ -20,6 +20,7 @@ import ( "github.com/TecharoHQ/anubis/internal/dnsbl" "github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/lib/challenge" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/web" @@ -155,7 +156,7 @@ func New(opts Options) (*Server, error) { if opts.Policy.Impressum != nil { registerWithPrefix(anubis.APIPrefix+"imprint", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { templ.Handler( - web.Base(opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum), + web.Base(opts.Policy.Impressum.Page.Title, opts.Policy.Impressum.Page, opts.Policy.Impressum, localization.GetLocalizer(r)), ).ServeHTTP(w, r) }), "GET") } diff --git a/lib/http.go b/lib/http.go index 664a934..1a5480c 100644 --- a/lib/http.go +++ b/lib/http.go @@ -12,6 +12,7 @@ import ( "github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/lib/challenge" + "github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/web" "github.com/a-h/templ" @@ -83,9 +84,11 @@ func randomChance(n int) bool { } func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) { + localizer := localization.GetLocalizer(r) + if returnHTTPStatusOnly { w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("Authorization required")) + w.Write([]byte(localizer.T("authorization_required"))) return } @@ -93,7 +96,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic 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") - s.respondWithError(w, r, "Client Error: Please ensure your browser is up to date and try again later.") + s.respondWithError(w, r, localizer.T("client_error_browser")) } challengesIssued.WithLabelValues("embedded").Add(1) @@ -118,7 +121,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic impl, ok := challenge.Get(rule.Challenge.Algorithm) if !ok { lg.Error("check failed", "err", "can't get algorithm", "algorithm", rule.Challenge.Algorithm) - s.respondWithError(w, r, fmt.Sprintf("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to file a bug as Anubis is trying to use challenge method %s but it does not exist in the challenge registry", rule.Challenge.Algorithm)) + s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm)) return } @@ -132,7 +135,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic component, err := impl.Issue(r, lg, in) if err != nil { lg.Error("[unexpected] render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this. - s.respondWithError(w, r, "Internal Server Error: please contact the administrator and ask them to look for the logs around \"RenderIndex\"") + s.respondWithError(w, r, fmt.Sprintf("%s \"RenderIndex\"", localizer.T("internal_server_error"))) return } @@ -144,8 +147,10 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic } func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) { + localizer := localization.GetLocalizer(r) + templ.Handler( - web.Base("Benchmarking Anubis!", web.Bench(), s.policy.Impressum), + web.Base(localizer.T("benchmarking_anubis"), web.Bench(localizer), s.policy.Impressum, localizer), ).ServeHTTP(w, r) } @@ -154,7 +159,9 @@ func (s *Server) respondWithError(w http.ResponseWriter, r *http.Request, messag } func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg string, status int) { - templ.Handler(web.Base("Oh noes!", web.ErrorPage(msg, s.opts.WebmasterEmail), s.policy.Impressum), templ.WithStatus(status)).ServeHTTP(w, r) + localizer := localization.GetLocalizer(r) + + templ.Handler(web.Base(localizer.T("oh_noes"), web.ErrorPage(msg, s.opts.WebmasterEmail, localizer), s.policy.Impressum, localizer), templ.WithStatus(status)).ServeHTTP(w, r) } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -189,15 +196,17 @@ func (s *Server) stripBasePrefixFromRequest(r *http.Request) *http.Request { func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) { if s.next == nil { + localizer := localization.GetLocalizer(r) + redir := r.FormValue("redir") urlParsed, err := r.URL.Parse(redir) if err != nil { - s.respondWithStatus(w, r, "Redirect URL not parseable", http.StatusBadRequest) + s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), http.StatusBadRequest) return } if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host { - s.respondWithStatus(w, r, "Redirect domain not allowed", http.StatusBadRequest) + s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), http.StatusBadRequest) return } @@ -207,7 +216,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) { } templ.Handler( - web.Base("You are not a bot!", web.StaticHappy(), s.policy.Impressum), + web.Base(localizer.T("you_are_not_a_bot"), web.StaticHappy(localizer), s.policy.Impressum, localizer), ).ServeHTTP(w, r) } else { requestsProxied.WithLabelValues(r.Host).Inc() diff --git a/lib/localization/locales/en.json b/lib/localization/locales/en.json new file mode 100644 index 0000000..1a9f0e0 --- /dev/null +++ b/lib/localization/locales/en.json @@ -0,0 +1,63 @@ +{ + "loading": "Loading...", + "why_am_i_seeing": "Why am I seeing this?", + "protected_by": "Protected by", + "made_with": "Made with ❤️ in 🇨🇦", + "mascot_design": "Mascot design by", + "ai_companies_explanation": "You are seeing this because the administrator of this website has set up Anubis to protect the server against the scourge of AI companies aggressively scraping websites. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.", + "anubis_compromise": "Anubis is a compromise. Anubis uses a Proof-of-Work scheme in the vein of Hashcash, a proposed proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.", + "hack_purpose": "Ultimately, this is a hack whose real purpose is to give a \"good enough\" placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to users that are much more likely to be legitimate.", + "jshelter_note": "Please note that Anubis requires the use of modern JavaScript features that plugins like JShelter will disable. Please disable JShelter or other such plugins for this domain.", + "version_info": "This website is running Anubis version", + "try_again": "Try again", + "go_home": "Go home", + "contact_webmaster": "or if you believe you should not be blocked, please contact the webmaster at", + "connection_security": "Please wait a moment while we ensure the security of your connection.", + "javascript_required": "Sadly, you must enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works. A no-JS solution is a work-in-progress.", + "benchmark_requires_js": "Running the benchmark tool requires JavaScript to be enabled.", + "difficulty": "Difficulty:", + "algorithm": "Algorithm:", + "compare": "Compare:", + "time": "Time", + "iters": "Iters", + "time_a": "Time A", + "iters_a": "Iters A", + "time_b": "Time B", + "iters_b": "Iters B", + "static_check_endpoint": "This is just a check endpoint for your reverse proxy to use.", + "authorization_required": "Authorization required", + "cookies_disabled": "Your browser is configured to disable cookies. Anubis requires cookies for the legitimate interest of making sure you are a valid client. Please enable cookies for this domain", + "access_denied": "Access Denied: error code", + "dronebl_entry": "DroneBL reported an entry", + "see_dronebl_lookup": "see", + "internal_server_error": "Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around", + "invalid_redirect": "Invalid redirect", + "redirect_not_parseable": "Redirect URL not parseable", + "redirect_domain_not_allowed": "Redirect domain not allowed", + "failed_to_sign_jwt": "failed to sign JWT", + "invalid_invocation": "Invalid invocation of MakeChallenge", + "client_error_browser": "Client Error: Please ensure your browser is up to date and try again later.", + "oh_noes": "Oh noes!", + "benchmarking_anubis": "Benchmarking Anubis!", + "you_are_not_a_bot": "You are not a bot!", + "making_sure_not_bot": "Making sure you're not a bot!", + "celphase": "CELPHASE", + "js_web_crypto_error": "Your browser doesn't have a functioning web.crypto element. Are you viewing this over a secure context?", + "js_web_workers_error": "Your browser doesn't support web workers (Anubis uses this to avoid freezing your browser). Do you have a plugin like JShelter installed?", + "js_cookies_error": "Your browser doesn't store cookies. Anubis uses cookies to determine which clients have passed challenges by storing a signed token in a cookie. Please enable storing cookies for this domain. The names of the cookies Anubis stores may vary without notice. Cookie names and values are not part of the public API.", + "js_context_not_secure": "Your context is not secure!", + "js_context_not_secure_msg": "Try connecting over HTTPS or let the admin know to set up HTTPS. For more information, see MDN.", + "js_calculating": "Calculating...", + "js_missing_feature": "Missing feature", + "js_challenge_error": "Challenge error!", + "js_challenge_error_msg": "Failed to resolve check algorithm. You may want to reload the page.", + "js_calculating_difficulty": "Calculating...Loading...
+{ localizer.T("loading") }
- You are seeing this because the administrator of this website has set up Anubis to protect the server against the scourge of - - AI companies - aggressively scraping websites - . This can and does cause downtime for the websites, which makes their - resources inaccessible for everyone. + { localizer.T("ai_companies_explanation") }
- Anubis is a compromise. Anubis uses a Proof-of-Work - scheme in the vein of Hashcash, a proposed - proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is - ignorable, but at mass scraper levels it adds up and makes scraping much more expensive. + { localizer.T("anubis_compromise") }
- Ultimately, this is a hack whose real purpose is to give a "good enough" placeholder solution so that more - time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) - so that the challenge proof of work page doesn't need to be presented to users that are much more likely to - be legitimate. + { localizer.T("hack_purpose") }
- Please note that Anubis requires the use of modern JavaScript features that plugins like JShelter will disable. Please disable JShelter or other such - plugins for this domain. + { localizer.T("jshelter_note") }
-This website is running Anubis version { anubis.Version }
.
{ localizer.T("version_info") } { anubis.Version }
.
{ message }.
- + if mail != "" {- Go home or if you believe you should not be blocked, please contact the webmaster at + { localizer.T("go_home") } { localizer.T("contact_webmaster") } { mail }
} else { - + }This is just a check endpoint for your reverse proxy to use.
+{ localizer.T("static_check_endpoint") }
Time | -Iters | +{ localizer.T("time") } | +{ localizer.T("iters") } |
---|