mirror of
https://github.com/TecharoHQ/anubis.git
synced 2025-09-10 21:26:11 -04:00
refactor(lib): Split up anubis.go into some smaller files. (#379)
* refactor(logging): centralize logger creation in GetLogger function Signed-off-by: Jason Cameron <git@jasoncameron.dev> * refactor(logging): rename GetLogger to GetRequestLogger for clarity Signed-off-by: Jason Cameron <git@jasoncameron.dev> * refactor: streamline error handling and response methods Signed-off-by: Jason Cameron <git@jasoncameron.dev> * refactor(lib): Split anubis.go up into some smaller specialized methods Signed-off-by: Jason Cameron <git@jasoncameron.dev> * refactor(http): simplify error response handling by using respondWithStatus Signed-off-by: Jason Cameron <git@jasoncameron.dev> * chore(lib): run goimports Signed-off-by: Xe Iaso <me@xeiaso.net> --------- Signed-off-by: Jason Cameron <git@jasoncameron.dev> Signed-off-by: Xe Iaso <me@xeiaso.net> Co-authored-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
parent
755c18a9a7
commit
301c7a42bd
5
go.mod
5
go.mod
@ -40,9 +40,9 @@ require (
|
|||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect
|
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect
|
||||||
golang.org/x/mod v0.24.0 // indirect
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
golang.org/x/sync v0.12.0 // indirect
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.31.0 // indirect
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.5 // indirect
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
honnef.co/go/tools v0.6.1 // indirect
|
honnef.co/go/tools v0.6.1 // indirect
|
||||||
k8s.io/apimachinery v0.32.3 // indirect
|
k8s.io/apimachinery v0.32.3 // indirect
|
||||||
@ -52,6 +52,7 @@ require (
|
|||||||
|
|
||||||
tool (
|
tool (
|
||||||
github.com/a-h/templ/cmd/templ
|
github.com/a-h/templ/cmd/templ
|
||||||
|
golang.org/x/tools/cmd/goimports
|
||||||
golang.org/x/tools/cmd/stringer
|
golang.org/x/tools/cmd/stringer
|
||||||
honnef.co/go/tools/cmd/staticcheck
|
honnef.co/go/tools/cmd/staticcheck
|
||||||
)
|
)
|
||||||
|
4
go.sum
4
go.sum
@ -99,6 +99,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@ -128,6 +130,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||||
|
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||||
|
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
@ -3,6 +3,7 @@ package internal
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,3 +23,14 @@ func InitSlog(level string) {
|
|||||||
})
|
})
|
||||||
slog.SetDefault(slog.New(h))
|
slog.SetDefault(slog.New(h))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetRequestLogger(r *http.Request) *slog.Logger {
|
||||||
|
return slog.With(
|
||||||
|
"user_agent", r.UserAgent(),
|
||||||
|
"accept_language", r.Header.Get("Accept-Language"),
|
||||||
|
"priority", r.Header.Get("Priority"),
|
||||||
|
"x-forwarded-for",
|
||||||
|
r.Header.Get("X-Forwarded-For"),
|
||||||
|
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
335
lib/anubis.go
335
lib/anubis.go
@ -2,38 +2,31 @@ package lib
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
|
||||||
"github.com/TecharoHQ/anubis"
|
"github.com/TecharoHQ/anubis"
|
||||||
"github.com/TecharoHQ/anubis/data"
|
|
||||||
"github.com/TecharoHQ/anubis/decaymap"
|
"github.com/TecharoHQ/anubis/decaymap"
|
||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy"
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||||
"github.com/TecharoHQ/anubis/web"
|
|
||||||
"github.com/TecharoHQ/anubis/xess"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -64,121 +57,6 @@ var (
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
Next http.Handler
|
|
||||||
Policy *policy.ParsedConfig
|
|
||||||
RedirectDomains []string
|
|
||||||
ServeRobotsTXT bool
|
|
||||||
PrivateKey ed25519.PrivateKey
|
|
||||||
|
|
||||||
CookieDomain string
|
|
||||||
CookieName string
|
|
||||||
CookiePartitioned bool
|
|
||||||
|
|
||||||
OGPassthrough bool
|
|
||||||
OGTimeToLive time.Duration
|
|
||||||
Target string
|
|
||||||
|
|
||||||
WebmasterEmail string
|
|
||||||
BasePrefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
|
||||||
var fin io.ReadCloser
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if fname != "" {
|
|
||||||
fin, err = os.Open(fname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fname = "(data)/botPolicies.yaml"
|
|
||||||
fin, err = data.BotPolicies.Open("botPolicies.yaml")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("[unexpected] can't parse builtin policy file %s: %w", fname, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func(fin io.ReadCloser) {
|
|
||||||
err := fin.Close()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to close policy file", "file", fname, "err", err)
|
|
||||||
}
|
|
||||||
}(fin)
|
|
||||||
|
|
||||||
anubisPolicy, err := policy.ParseConfig(fin, fname, defaultDifficulty)
|
|
||||||
|
|
||||||
return anubisPolicy, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(opts Options) (*Server, error) {
|
|
||||||
if opts.PrivateKey == nil {
|
|
||||||
slog.Debug("opts.PrivateKey not set, generating a new one")
|
|
||||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("lib: can't generate private key: %v", err)
|
|
||||||
}
|
|
||||||
opts.PrivateKey = priv
|
|
||||||
}
|
|
||||||
|
|
||||||
anubis.BasePrefix = opts.BasePrefix
|
|
||||||
|
|
||||||
result := &Server{
|
|
||||||
next: opts.Next,
|
|
||||||
priv: opts.PrivateKey,
|
|
||||||
pub: opts.PrivateKey.Public().(ed25519.PublicKey),
|
|
||||||
policy: opts.Policy,
|
|
||||||
opts: opts,
|
|
||||||
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
|
|
||||||
OGTags: ogtags.NewOGTagCache(opts.Target, opts.OGPassthrough, opts.OGTimeToLive),
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
xess.Mount(mux)
|
|
||||||
|
|
||||||
// Helper to add global prefix
|
|
||||||
registerWithPrefix := func(pattern string, handler http.Handler, method string) {
|
|
||||||
if method != "" {
|
|
||||||
method = method + " " // methods must end with a space to register with them
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure there's no double slash when concatenating BasePrefix and pattern
|
|
||||||
basePrefix := strings.TrimSuffix(anubis.BasePrefix, "/")
|
|
||||||
prefix := method + basePrefix
|
|
||||||
|
|
||||||
// If pattern doesn't start with a slash, add one
|
|
||||||
if !strings.HasPrefix(pattern, "/") {
|
|
||||||
pattern = "/" + pattern
|
|
||||||
}
|
|
||||||
|
|
||||||
mux.Handle(prefix+pattern, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure there's no double slash when concatenating BasePrefix and StaticPath
|
|
||||||
stripPrefix := strings.TrimSuffix(anubis.BasePrefix, "/") + anubis.StaticPath
|
|
||||||
registerWithPrefix(anubis.StaticPath, internal.UnchangingCache(internal.NoBrowsing(http.StripPrefix(stripPrefix, http.FileServerFS(web.Static)))), "")
|
|
||||||
|
|
||||||
if opts.ServeRobotsTXT {
|
|
||||||
registerWithPrefix("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
|
|
||||||
}), "GET")
|
|
||||||
registerWithPrefix("/.well-known/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
|
|
||||||
}), "GET")
|
|
||||||
}
|
|
||||||
|
|
||||||
registerWithPrefix(anubis.APIPrefix+"make-challenge", http.HandlerFunc(result.MakeChallenge), "POST")
|
|
||||||
registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET")
|
|
||||||
registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
|
|
||||||
registerWithPrefix(anubis.APIPrefix+"test-error", http.HandlerFunc(result.TestError), "GET")
|
|
||||||
registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
|
|
||||||
|
|
||||||
result.mux = mux
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
next http.Handler
|
next http.Handler
|
||||||
@ -190,40 +68,6 @@ type Server struct {
|
|||||||
OGTags *ogtags.OGTagCache
|
OGTags *ogtags.OGTagCache
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
func (s *Server) challengeFor(r *http.Request, difficulty int) string {
|
||||||
fp := sha256.Sum256(s.priv.Seed())
|
fp := sha256.Sum256(s.priv.Seed())
|
||||||
|
|
||||||
@ -248,19 +92,12 @@ func (s *Server) maybeReverseProxyOrPage(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
|
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
|
||||||
lg := slog.With(
|
lg := internal.GetRequestLogger(r)
|
||||||
"user_agent", r.UserAgent(),
|
|
||||||
"accept_language", r.Header.Get("Accept-Language"),
|
|
||||||
"priority", r.Header.Get("Priority"),
|
|
||||||
"x-forwarded-for",
|
|
||||||
r.Header.Get("X-Forwarded-For"),
|
|
||||||
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
|
||||||
)
|
|
||||||
|
|
||||||
cr, rule, err := s.check(r)
|
cr, rule, err := s.check(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("check failed", "err", err)
|
lg.Error("check failed", "err", err)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"maybeReverseProxy\"", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
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\"")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,52 +108,11 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
|||||||
|
|
||||||
ip := r.Header.Get("X-Real-Ip")
|
ip := r.Header.Get("X-Real-Ip")
|
||||||
|
|
||||||
if s.policy.DNSBL && ip != "" {
|
if s.handleDNSBL(w, r, ip, lg) {
|
||||||
resp, ok := s.DNSBLCache.Get(ip)
|
return
|
||||||
if !ok {
|
|
||||||
lg.Debug("looking up ip in dnsbl")
|
|
||||||
resp, err := dnsbl.Lookup(ip)
|
|
||||||
if err != nil {
|
|
||||||
lg.Error("can't look up ip in dnsbl", "err", err)
|
|
||||||
}
|
|
||||||
s.DNSBLCache.Set(ip, resp, 24*time.Hour)
|
|
||||||
droneBLHits.WithLabelValues(resp.String()).Inc()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp != dnsbl.AllGood {
|
if s.checkRules(w, r, cr, lg, rule) {
|
||||||
lg.Info("DNSBL hit", "status", resp.String())
|
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage(fmt.Sprintf("DroneBL reported an entry: %s, see https://dronebl.org/lookup?ip=%s", resp.String(), ip), s.opts.WebmasterEmail)), templ.WithStatus(http.StatusOK)).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cr.Rule {
|
|
||||||
case config.RuleAllow:
|
|
||||||
lg.Debug("allowing traffic to origin (explicit)")
|
|
||||||
s.ServeHTTPNext(w, r)
|
|
||||||
return
|
|
||||||
case config.RuleDeny:
|
|
||||||
s.ClearCookie(w)
|
|
||||||
lg.Info("explicit deny")
|
|
||||||
if rule == nil {
|
|
||||||
lg.Error("rule is nil, cannot calculate checksum")
|
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hash := rule.Hash()
|
|
||||||
|
|
||||||
lg.Debug("rule hash", "hash", hash)
|
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage(fmt.Sprintf("Access Denied: error code %s", hash), s.opts.WebmasterEmail)), templ.WithStatus(http.StatusOK)).ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
case config.RuleChallenge:
|
|
||||||
lg.Debug("challenge requested")
|
|
||||||
case config.RuleBenchmark:
|
|
||||||
lg.Debug("serving benchmark page")
|
|
||||||
s.RenderBench(w, r)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
s.ClearCookie(w)
|
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,53 +153,64 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
|||||||
s.ServeHTTPNext(w, r)
|
s.ServeHTTPNext(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) {
|
func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.CheckResult, lg *slog.Logger, rule *policy.Bot) bool {
|
||||||
if returnHTTPStatusOnly {
|
switch cr.Rule {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
case config.RuleAllow:
|
||||||
w.Write([]byte("Authorization required"))
|
lg.Debug("allowing traffic to origin (explicit)")
|
||||||
return
|
s.ServeHTTPNext(w, r)
|
||||||
|
return true
|
||||||
|
case config.RuleDeny:
|
||||||
|
s.ClearCookie(w)
|
||||||
|
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\"")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
hash := rule.Hash()
|
||||||
|
|
||||||
|
lg.Debug("rule hash", "hash", hash)
|
||||||
|
s.respondWithStatus(w, r, fmt.Sprintf("Access Denied: error code %s", hash), http.StatusOK)
|
||||||
|
return true
|
||||||
|
case config.RuleChallenge:
|
||||||
|
lg.Debug("challenge requested")
|
||||||
|
case config.RuleBenchmark:
|
||||||
|
lg.Debug("serving benchmark page")
|
||||||
|
s.RenderBench(w, r)
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
s.ClearCookie(w)
|
||||||
|
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\"")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
lg := slog.With(
|
func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string, lg *slog.Logger) bool {
|
||||||
"user_agent", r.UserAgent(),
|
if s.policy.DNSBL && ip != "" {
|
||||||
"accept_language", r.Header.Get("Accept-Language"),
|
resp, ok := s.DNSBLCache.Get(ip)
|
||||||
"priority", r.Header.Get("Priority"),
|
if !ok {
|
||||||
"x-forwarded-for",
|
lg.Debug("looking up ip in dnsbl")
|
||||||
r.Header.Get("X-Forwarded-For"),
|
resp, err := dnsbl.Lookup(ip)
|
||||||
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("failed to get OG tags", "err", err)
|
lg.Error("can't look up ip in dnsbl", "err", err)
|
||||||
ogTags = nil
|
|
||||||
}
|
}
|
||||||
|
s.DNSBLCache.Set(ip, resp, 24*time.Hour)
|
||||||
|
droneBLHits.WithLabelValues(resp.String()).Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags)
|
if resp != dnsbl.AllGood {
|
||||||
if err != nil {
|
lg.Info("DNSBL hit", "status", resp.String())
|
||||||
lg.Error("render failed", "err", err)
|
s.respondWithStatus(w, r, fmt.Sprintf("DroneBL reported an entry: %s, see https://dronebl.org/lookup?ip=%s", resp.String(), ip), http.StatusOK)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Other internal server error (contact the admin)", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
return true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := internal.NoStoreCache(templ.Handler(component))
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
func (s *Server) RenderBench(w http.ResponseWriter, r *http.Request) {
|
|
||||||
templ.Handler(
|
|
||||||
web.Base("Benchmarking Anubis!", web.Bench()),
|
|
||||||
).ServeHTTP(w, r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
lg := slog.With("user_agent", r.UserAgent(), "accept_language", r.Header.Get("Accept-Language"), "priority", r.Header.Get("Priority"), "x-forwarded-for", r.Header.Get("X-Forwarded-For"), "x-real-ip", r.Header.Get("X-Real-Ip"))
|
lg := internal.GetRequestLogger(r)
|
||||||
|
|
||||||
encoder := json.NewEncoder(w)
|
encoder := json.NewEncoder(w)
|
||||||
cr, rule, err := s.check(r)
|
cr, rule, err := s.check(r)
|
||||||
@ -441,19 +248,13 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
lg := slog.With(
|
lg := internal.GetRequestLogger(r)
|
||||||
"user_agent", r.UserAgent(),
|
|
||||||
"accept_language", r.Header.Get("Accept-Language"),
|
|
||||||
"priority", r.Header.Get("Priority"),
|
|
||||||
"x-forwarded-for", r.Header.Get("X-Forwarded-For"),
|
|
||||||
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
|
||||||
)
|
|
||||||
|
|
||||||
redir := r.FormValue("redir")
|
redir := r.FormValue("redir")
|
||||||
redirURL, err := url.ParseRequestURI(redir)
|
redirURL, err := url.ParseRequestURI(redir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("invalid redirect", "err", err)
|
lg.Error("invalid redirect", "err", err)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid redirect", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
s.respondWithError(w, r, "Invalid redirect")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// used by the path checker rule
|
// used by the path checker rule
|
||||||
@ -462,7 +263,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
cr, rule, err := s.check(r)
|
cr, rule, err := s.check(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("check failed", "err", err)
|
lg.Error("check failed", "err", err)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\".", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
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\".\"")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lg = lg.With("check_result", cr)
|
lg = lg.With("check_result", cr)
|
||||||
@ -471,7 +272,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
if nonceStr == "" {
|
if nonceStr == "" {
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("no nonce")
|
lg.Debug("no nonce")
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing nonce", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
s.respondWithError(w, r, "missing nonce")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -479,7 +280,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
if elapsedTimeStr == "" {
|
if elapsedTimeStr == "" {
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("no elapsedTime")
|
lg.Debug("no elapsedTime")
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("missing elapsedTime", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
s.respondWithError(w, r, "missing elapsedTime")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,7 +288,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("elapsedTime doesn't parse", "err", err)
|
lg.Debug("elapsedTime doesn't parse", "err", err)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid elapsedTime", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
s.respondWithError(w, r, "invalid elapsedTime")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,15 +298,11 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
response := r.FormValue("response")
|
response := r.FormValue("response")
|
||||||
urlParsed, err := r.URL.Parse(redir)
|
urlParsed, err := r.URL.Parse(redir)
|
||||||
if err != nil {
|
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)
|
s.respondWithError(w, r, "Redirect URL not parseable")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host)) || urlParsed.Host != r.URL.Host {
|
||||||
if len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host) {
|
s.respondWithError(w, r, "Redirect domain not allowed")
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -515,7 +312,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("nonce doesn't parse", "err", err)
|
lg.Debug("nonce doesn't parse", "err", err)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid nonce", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
s.respondWithError(w, r, "invalid nonce")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -525,7 +322,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
|
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("hash does not match", "got", response, "want", calculated)
|
lg.Debug("hash does not match", "got", response, "want", calculated)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden)
|
||||||
failedValidations.Inc()
|
failedValidations.Inc()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -534,7 +331,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
|
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty)
|
lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
s.respondWithStatus(w, r, "invalid response", http.StatusForbidden)
|
||||||
failedValidations.Inc()
|
failedValidations.Inc()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -557,7 +354,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("failed to sign JWT", "err", err)
|
lg.Error("failed to sign JWT", "err", err)
|
||||||
s.ClearCookie(w)
|
s.ClearCookie(w)
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("failed to sign JWT", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
s.respondWithError(w, r, "failed to sign JWT")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -578,7 +375,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) TestError(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) TestError(w http.ResponseWriter, r *http.Request) {
|
||||||
err := r.FormValue("err")
|
err := r.FormValue("err")
|
||||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage(err, s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
s.respondWithError(w, r, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cr(name string, rule config.Rule) policy.CheckResult {
|
func cr(name string, rule config.Rule) policy.CheckResult {
|
||||||
|
138
lib/config.go
Normal file
138
lib/config.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis"
|
||||||
|
"github.com/TecharoHQ/anubis/data"
|
||||||
|
"github.com/TecharoHQ/anubis/decaymap"
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
"github.com/TecharoHQ/anubis/internal/dnsbl"
|
||||||
|
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
"github.com/TecharoHQ/anubis/web"
|
||||||
|
"github.com/TecharoHQ/anubis/xess"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Next http.Handler
|
||||||
|
Policy *policy.ParsedConfig
|
||||||
|
RedirectDomains []string
|
||||||
|
ServeRobotsTXT bool
|
||||||
|
PrivateKey ed25519.PrivateKey
|
||||||
|
|
||||||
|
CookieDomain string
|
||||||
|
CookieName string
|
||||||
|
CookiePartitioned bool
|
||||||
|
|
||||||
|
OGPassthrough bool
|
||||||
|
OGTimeToLive time.Duration
|
||||||
|
Target string
|
||||||
|
|
||||||
|
WebmasterEmail string
|
||||||
|
BasePrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
||||||
|
var fin io.ReadCloser
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if fname != "" {
|
||||||
|
fin, err = os.Open(fname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fname = "(data)/botPolicies.yaml"
|
||||||
|
fin, err = data.BotPolicies.Open("botPolicies.yaml")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("[unexpected] can't parse builtin policy file %s: %w", fname, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(fin io.ReadCloser) {
|
||||||
|
err := fin.Close()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to close policy file", "file", fname, "err", err)
|
||||||
|
}
|
||||||
|
}(fin)
|
||||||
|
|
||||||
|
anubisPolicy, err := policy.ParseConfig(fin, fname, defaultDifficulty)
|
||||||
|
|
||||||
|
return anubisPolicy, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts Options) (*Server, error) {
|
||||||
|
if opts.PrivateKey == nil {
|
||||||
|
slog.Debug("opts.PrivateKey not set, generating a new one")
|
||||||
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("lib: can't generate private key: %v", err)
|
||||||
|
}
|
||||||
|
opts.PrivateKey = priv
|
||||||
|
}
|
||||||
|
|
||||||
|
anubis.BasePrefix = opts.BasePrefix
|
||||||
|
|
||||||
|
result := &Server{
|
||||||
|
next: opts.Next,
|
||||||
|
priv: opts.PrivateKey,
|
||||||
|
pub: opts.PrivateKey.Public().(ed25519.PublicKey),
|
||||||
|
policy: opts.Policy,
|
||||||
|
opts: opts,
|
||||||
|
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
|
||||||
|
OGTags: ogtags.NewOGTagCache(opts.Target, opts.OGPassthrough, opts.OGTimeToLive),
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
xess.Mount(mux)
|
||||||
|
|
||||||
|
// Helper to add global prefix
|
||||||
|
registerWithPrefix := func(pattern string, handler http.Handler, method string) {
|
||||||
|
if method != "" {
|
||||||
|
method = method + " " // methods must end with a space to register with them
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure there's no double slash when concatenating BasePrefix and pattern
|
||||||
|
basePrefix := strings.TrimSuffix(anubis.BasePrefix, "/")
|
||||||
|
prefix := method + basePrefix
|
||||||
|
|
||||||
|
// If pattern doesn't start with a slash, add one
|
||||||
|
if !strings.HasPrefix(pattern, "/") {
|
||||||
|
pattern = "/" + pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.Handle(prefix+pattern, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure there's no double slash when concatenating BasePrefix and StaticPath
|
||||||
|
stripPrefix := strings.TrimSuffix(anubis.BasePrefix, "/") + anubis.StaticPath
|
||||||
|
registerWithPrefix(anubis.StaticPath, internal.UnchangingCache(internal.NoBrowsing(http.StripPrefix(stripPrefix, http.FileServerFS(web.Static)))), "")
|
||||||
|
|
||||||
|
if opts.ServeRobotsTXT {
|
||||||
|
registerWithPrefix("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
|
||||||
|
}), "GET")
|
||||||
|
registerWithPrefix("/.well-known/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFileFS(w, r, web.Static, "static/robots.txt")
|
||||||
|
}), "GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
registerWithPrefix(anubis.APIPrefix+"make-challenge", http.HandlerFunc(result.MakeChallenge), "POST")
|
||||||
|
registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET")
|
||||||
|
registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "")
|
||||||
|
registerWithPrefix(anubis.APIPrefix+"test-error", http.HandlerFunc(result.TestError), "GET")
|
||||||
|
registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "")
|
||||||
|
|
||||||
|
result.mux = mux
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
82
lib/http.go
82
lib/http.go
@ -2,8 +2,14 @@ package lib
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
"github.com/TecharoHQ/anubis/web"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
|
||||||
"github.com/TecharoHQ/anubis"
|
"github.com/TecharoHQ/anubis"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,3 +39,79 @@ func (t UnixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||||||
req.URL.Scheme = "http" // make http.Transport happy and avoid an infinite recursion
|
req.URL.Scheme = "http" // make http.Transport happy and avoid an infinite recursion
|
||||||
return t.Transport.RoundTrip(req)
|
return t.Transport.RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
lg.Error("failed to get OG tags", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.NoStoreCache(templ.Handler(component))
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user