fix(lib): show error message detail when hitting some common flows

Instead of giving the user nothing to go off of, this patch gives them
an opaque blob of ROT-13 encoded base64. The logic is that if you are
smart enough to figure out how to decode this, you're probably smart
enough to either fix your broken client or give it to the adminstrator.

Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Xe Iaso
2025-10-21 16:41:49 +00:00
parent 00261d049e
commit df0dd98c72
5 changed files with 220 additions and 151 deletions

View File

@@ -1,6 +1,9 @@
package lib
import (
"bytes"
"compress/gzip"
"encoding/base64"
"errors"
"fmt"
"math/rand"
@@ -25,6 +28,10 @@ import (
var domainMatchRegexp = regexp.MustCompile(`^((xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`)
var (
ErrActualAnubisBug = errors.New("this is an actual bug in Anubis, please file an issue with the magic string 'taco bell'")
)
// matchRedirectDomain returns true if host matches any of the allowed redirect
// domain patterns. Patterns may contain '*' which are matched using the
// internal glob matcher. Matching is case-insensitive on hostnames.
@@ -145,6 +152,46 @@ func randomChance(n int) bool {
return rand.Intn(n) == 0
}
// XXX(Xe): generated by ChatGPT
func rot13(s string) string {
rotated := make([]rune, len(s))
for i, c := range s {
switch {
case c >= 'A' && c <= 'Z':
rotated[i] = 'A' + ((c - 'A' + 13) % 26)
case c >= 'a' && c <= 'z':
rotated[i] = 'a' + ((c - 'a' + 13) % 26)
default:
rotated[i] = c
}
}
return string(rotated)
}
func makeCode(err error) string {
var buf bytes.Buffer
gzw := gzip.NewWriter(&buf)
errStr := fmt.Sprintf("internal error: %v", err)
fmt.Fprintln(gzw, rot13(errStr))
if err := gzw.Close(); err != nil {
panic("can't write to gzip in ram buffer")
}
const width = 16
enc := base64.StdEncoding.EncodeToString(buf.Bytes())
var builder strings.Builder
for i := 0; i < len(enc); i += width {
end := i + width
if end > len(enc) {
end = len(enc)
}
builder.WriteString(enc[i:end])
builder.WriteByte('\n')
}
return builder.String()
}
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.CheckResult, rule *policy.Bot, returnHTTPStatusOnly bool) {
localizer := localization.GetLocalizer(r)
@@ -155,7 +202,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
} else {
redirectURL, err := s.constructRedirectURL(r)
if err != nil {
s.respondWithStatus(w, r, err.Error(), http.StatusBadRequest)
s.respondWithStatus(w, r, err.Error(), "", http.StatusBadRequest)
return
}
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
@@ -167,7 +214,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
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, localizer.T("client_error_browser"))
s.respondWithError(w, r, localizer.T("client_error_browser"), "")
return
}
@@ -176,7 +223,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
if err != nil {
lg.Error("can't get challenge", "err", err)
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm), makeCode(err))
return
}
@@ -203,7 +250,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
if !ok {
lg.Error("check failed", "err", "can't get algorithm", "algorithm", rule.Challenge.Algorithm)
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm))
s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), rule.Challenge.Algorithm), makeCode(err))
return
}
@@ -218,7 +265,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
component, err := impl.Issue(w, r, lg, in)
if err != nil {
lg.Error("[unexpected] challenge component render failed, please open an issue", "err", err) // This is likely a bug in the template. Should never be triggered as CI tests for this.
s.respondWithError(w, r, fmt.Sprintf("%s \"RenderIndex\"", localizer.T("internal_server_error")))
s.respondWithError(w, r, fmt.Sprintf("%s \"RenderIndex\"", localizer.T("internal_server_error")), makeCode(err))
return
}
@@ -269,14 +316,14 @@ func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) {
).ServeHTTP(w, r)
}
func (s *Server) respondWithError(w http.ResponseWriter, r *http.Request, message string) {
s.respondWithStatus(w, r, message, http.StatusInternalServerError)
func (s *Server) respondWithError(w http.ResponseWriter, r *http.Request, message, code string) {
s.respondWithStatus(w, r, message, code, http.StatusInternalServerError)
}
func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg string, status int) {
func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg, code string, status int) {
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)
templ.Handler(web.Base(localizer.T("oh_noes"), web.ErrorPage(msg, s.opts.WebmasterEmail, code, localizer), s.policy.Impressum, localizer), templ.WithStatus(status)).ServeHTTP(w, r)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -324,7 +371,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
redir := r.FormValue("redir")
urlParsed, err := r.URL.Parse(redir)
if err != nil {
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), http.StatusBadRequest)
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), makeCode(err), http.StatusBadRequest)
return
}
@@ -336,7 +383,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
if hostNotAllowed || hostMismatch {
lg := internal.GetRequestLogger(s.logger, r)
lg.Debug("domain not allowed", "domain", urlParsed.Host)
s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), http.StatusBadRequest)
s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), makeCode(err), http.StatusBadRequest)
return
}