diff options
| author | Sandro <sandro.jaeckel@gmail.com> | 2025-04-25 19:38:02 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-25 17:38:02 +0000 |
| commit | 6858f66a62416354a349d8090fcb45b5262056eb (patch) | |
| tree | 1529e5c1067b1aae25cafcfcf31d718ef8bd74fb /lib/anubis.go | |
| parent | a5d796c679e63abc4e56bebd564c966633a7d5ac (diff) | |
| download | anubis-6858f66a62416354a349d8090fcb45b5262056eb.tar.xz anubis-6858f66a62416354a349d8090fcb45b5262056eb.zip | |
Add check endpoint which can be used with nginx' auth_request function (#266)
* Add check endpoint which can be used with nginx' auth_request function
* feat(cmd): allow configuring redirect domains
* test: add test environment for the nginx_auth PR
This is a full local setup of the nginx_auth PR including HTTPS so that
it's easier to validate in isolation.
This requires an install of k3s (https://k3s.io) with traefik set to
listen on localhost. This will be amended in the future but for now this
works enough to ship it.
Signed-off-by: Xe Iaso <me@xeiaso.net>
* fix(cmd|lib): allow empty redirect domains variable
Signed-off-by: Xe Iaso <me@xeiaso.net>
* fix(test): add space to target variable in anubis container
Signed-off-by: Xe Iaso <me@xeiaso.net>
* docs(admin): rewrite subrequest auth docs, make generic
* docs(install): document REDIRECT_DOMAINS flag
Signed-off-by: Xe Iaso <me@xeiaso.net>
* feat(lib): clamp redirects to the same HTTP host
Only if REDIRECT_DOMAINS is not set.
Signed-off-by: Xe Iaso <me@xeiaso.net>
---------
Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'lib/anubis.go')
| -rw-r--r-- | lib/anubis.go | 94 |
1 files changed, 77 insertions, 17 deletions
diff --git a/lib/anubis.go b/lib/anubis.go index f6445fb..8ca6964 100644 --- a/lib/anubis.go +++ b/lib/anubis.go @@ -14,6 +14,7 @@ import ( "net/http" "net/url" "os" + "slices" "strconv" "strings" "time" @@ -64,10 +65,11 @@ var ( ) type Options struct { - Next http.Handler - Policy *policy.ParsedConfig - ServeRobotsTXT bool - PrivateKey ed25519.PrivateKey + Next http.Handler + Policy *policy.ParsedConfig + RedirectDomains []string + ServeRobotsTXT bool + PrivateKey ed25519.PrivateKey CookieDomain string CookieName string @@ -148,9 +150,10 @@ func New(opts Options) (*Server, error) { mux.HandleFunc("POST /.within.website/x/cmd/anubis/api/make-challenge", result.MakeChallenge) mux.HandleFunc("GET /.within.website/x/cmd/anubis/api/pass-challenge", result.PassChallenge) + mux.HandleFunc("GET /.within.website/x/cmd/anubis/api/check", result.maybeReverseProxyHttpStatusOnly) mux.HandleFunc("GET /.within.website/x/cmd/anubis/api/test-error", result.TestError) - mux.HandleFunc("/", result.MaybeReverseProxy) + mux.HandleFunc("/", result.maybeReverseProxyOrPage) result.mux = mux @@ -172,6 +175,36 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) } +func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) { + if s.next == nil { + redir := r.FormValue("redir") + urlParsed, err := r.URL.Parse(redir) + if err != nil { + templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect URL not parseable", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r) + return + } + + if len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host) { + templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r) + return + } else if urlParsed.Host != r.URL.Host { + templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r) + return + } + + if redir != "" { + http.Redirect(w, r, redir, http.StatusFound) + return + } + + templ.Handler( + web.Base("You are not a bot!", web.StaticHappy()), + ).ServeHTTP(w, r) + } else { + s.next.ServeHTTP(w, r) + } +} + func (s *Server) challengeFor(r *http.Request, difficulty int) string { fp := sha256.Sum256(s.priv.Seed()) @@ -187,7 +220,15 @@ func (s *Server) challengeFor(r *http.Request, difficulty int) string { return internal.SHA256sum(challengeData) } -func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) { +func (s *Server) maybeReverseProxyHttpStatusOnly(w http.ResponseWriter, r *http.Request) { + s.maybeReverseProxy(w, r, true) +} + +func (s *Server) maybeReverseProxyOrPage(w http.ResponseWriter, r *http.Request) { + s.maybeReverseProxy(w, r, false) +} + +func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) { lg := slog.With( "user_agent", r.UserAgent(), "accept_language", r.Header.Get("Accept-Language"), @@ -233,7 +274,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) { switch cr.Rule { case config.RuleAllow: lg.Debug("allowing traffic to origin (explicit)") - s.next.ServeHTTP(w, r) + s.ServeHTTPNext(w, r) return case config.RuleDeny: s.ClearCookie(w) @@ -264,21 +305,21 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) { if err != nil { lg.Debug("cookie not found", "path", r.URL.Path) s.ClearCookie(w) - s.RenderIndex(w, r, rule) + s.RenderIndex(w, r, rule, httpStatusOnly) return } if err := ckie.Valid(); err != nil { lg.Debug("cookie is invalid", "err", err) s.ClearCookie(w) - s.RenderIndex(w, r, rule) + s.RenderIndex(w, r, rule, httpStatusOnly) return } if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() { lg.Debug("cookie expired", "path", r.URL.Path) s.ClearCookie(w) - s.RenderIndex(w, r, rule) + s.RenderIndex(w, r, rule, httpStatusOnly) return } @@ -289,14 +330,14 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) { if err != nil || !token.Valid { lg.Debug("invalid token", "path", r.URL.Path, "err", err) s.ClearCookie(w) - s.RenderIndex(w, r, rule) + s.RenderIndex(w, r, rule, httpStatusOnly) return } if randomJitter() { r.Header.Add("X-Anubis-Status", "PASS-BRIEF") lg.Debug("cookie is not enrolled into secondary screening") - s.next.ServeHTTP(w, r) + s.ServeHTTPNext(w, r) return } @@ -304,7 +345,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) { if !ok { lg.Debug("invalid token claims type", "path", r.URL.Path) s.ClearCookie(w) - s.RenderIndex(w, r, rule) + s.RenderIndex(w, r, rule, httpStatusOnly) return } challenge := s.challengeFor(r, rule.Challenge.Difficulty) @@ -312,7 +353,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) { if claims["challenge"] != challenge { lg.Debug("invalid challenge", "path", r.URL.Path) s.ClearCookie(w) - s.RenderIndex(w, r, rule) + s.RenderIndex(w, r, rule, httpStatusOnly) return } @@ -329,16 +370,22 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) { lg.Debug("invalid response", "path", r.URL.Path) failedValidations.Inc() s.ClearCookie(w) - s.RenderIndex(w, r, rule) + s.RenderIndex(w, r, rule, httpStatusOnly) return } slog.Debug("all checks passed") r.Header.Add("X-Anubis-Status", "PASS-FULL") - s.next.ServeHTTP(w, r) + s.ServeHTTPNext(w, r) } -func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot) { +func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) { + if returnHTTPStatusOnly { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Authorization required")) + return + } + lg := slog.With( "user_agent", r.UserAgent(), "accept_language", r.Header.Get("Accept-Language"), @@ -470,6 +517,19 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { timeTaken.Observe(elapsedTime) response := r.FormValue("response") + urlParsed, err := r.URL.Parse(redir) + if err != nil { + templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect URL not parseable", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r) + return + } + + if len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host) { + templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r) + return + } else if urlParsed.Host != r.URL.Host { + templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r) + return + } challenge := s.challengeFor(r, rule.Challenge.Difficulty) |
