aboutsummaryrefslogtreecommitdiff
path: root/lib/anubis.go
diff options
context:
space:
mode:
authorSandro <sandro.jaeckel@gmail.com>2025-04-25 19:38:02 +0200
committerGitHub <noreply@github.com>2025-04-25 17:38:02 +0000
commit6858f66a62416354a349d8090fcb45b5262056eb (patch)
tree1529e5c1067b1aae25cafcfcf31d718ef8bd74fb /lib/anubis.go
parenta5d796c679e63abc4e56bebd564c966633a7d5ac (diff)
downloadanubis-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.go94
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)