mirror of
https://github.com/TecharoHQ/anubis.git
synced 2025-08-03 17:59:24 -04:00

Closes #520 For some reason, Chrome and Firefox are very picky over what they use to match cookies that need to be deleted. Listen to me for my tale of woe: The basic problem here is that cookies were an early hack added on the side of the HTTP spec and they're basically impossible to upgrade or change because who knows what relies on the exact behavior cookies use. As a result, cookies don't just match by name, but by every setting that exists on them. You can also have two cookies with the same name but different values. This spec is a nightmare lol. Even more fun: browsers will make up values for cookies if they aren't set, meaning that getting a challenge token at `/docs` is semantically different than a challenge token you got from `/`. This PR fixes this issue by explicitly setting the "make sure cookie support is working" cookie's path to `/`, meaning that it will always be sent. Additionally, cookies are expired by setting the expiry time to one minute in the past. Hopefully this will fix it. I'm testing this locally and it seems to work fine. Signed-off-by: Xe Iaso <me@xeiaso.net>
152 lines
4.4 KiB
Go
152 lines
4.4 KiB
Go
package lib
|
|
|
|
import (
|
|
"math/rand"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/TecharoHQ/anubis"
|
|
"github.com/TecharoHQ/anubis/internal"
|
|
"github.com/TecharoHQ/anubis/lib/policy"
|
|
"github.com/TecharoHQ/anubis/web"
|
|
"github.com/a-h/templ"
|
|
)
|
|
|
|
func (s *Server) SetCookie(w http.ResponseWriter, name, value, path string) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: name,
|
|
Value: value,
|
|
Expires: time.Now().Add(s.opts.CookieExpiration),
|
|
SameSite: http.SameSiteLaxMode,
|
|
Domain: s.opts.CookieDomain,
|
|
Partitioned: s.opts.CookiePartitioned,
|
|
Path: path,
|
|
})
|
|
}
|
|
|
|
func (s *Server) ClearCookie(w http.ResponseWriter, name, path string) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: name,
|
|
Value: "",
|
|
MaxAge: -1,
|
|
Expires: time.Now().Add(-1 * time.Minute),
|
|
SameSite: http.SameSiteLaxMode,
|
|
Partitioned: s.opts.CookiePartitioned,
|
|
Domain: s.opts.CookieDomain,
|
|
Path: path,
|
|
})
|
|
}
|
|
|
|
// https://github.com/oauth2-proxy/oauth2-proxy/blob/master/pkg/upstream/http.go#L124
|
|
type UnixRoundTripper struct {
|
|
Transport *http.Transport
|
|
}
|
|
|
|
// set bare minimum stuff
|
|
func (t UnixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
req = req.Clone(req.Context())
|
|
if req.Host == "" {
|
|
req.Host = "localhost"
|
|
}
|
|
req.URL.Host = req.Host // proxy error: no Host in request URL
|
|
req.URL.Scheme = "http" // make http.Transport happy and avoid an infinite recursion
|
|
return t.Transport.RoundTrip(req)
|
|
}
|
|
|
|
func randomChance(n int) bool {
|
|
return rand.Intn(n) == 0
|
|
}
|
|
|
|
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 := internal.GetRequestLogger(r)
|
|
|
|
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.")
|
|
}
|
|
|
|
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
|
|
|
var ogTags map[string]string = nil
|
|
if s.opts.OGPassthrough {
|
|
var err error
|
|
ogTags, err = s.OGTags.GetOGTags(r.URL, r.Host)
|
|
if err != nil {
|
|
lg.Error("failed to get OG tags", "err", err)
|
|
}
|
|
}
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: anubis.TestCookieName,
|
|
Value: challenge,
|
|
Expires: time.Now().Add(30 * time.Minute),
|
|
Path: "/",
|
|
})
|
|
|
|
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags)
|
|
if err != nil {
|
|
lg.Error("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\"")
|
|
return
|
|
}
|
|
|
|
handler := internal.GzipMiddleware(1, internal.NoStoreCache(templ.Handler(
|
|
component,
|
|
templ.WithStatus(s.opts.Policy.StatusCodes.Challenge),
|
|
)))
|
|
handler.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) {
|
|
templ.Handler(
|
|
web.Base("Benchmarking Anubis!", web.Bench()),
|
|
).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) respondWithStatus(w http.ResponseWriter, r *http.Request, msg string, status int) {
|
|
templ.Handler(web.Base("Oh noes!", web.ErrorPage(msg, s.opts.WebmasterEmail)), templ.WithStatus(status)).ServeHTTP(w, r)
|
|
}
|
|
|
|
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 {
|
|
s.respondWithStatus(w, r, "Redirect URL 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)
|
|
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)
|
|
}
|
|
}
|