Anubis module for Caddy
1package caddy_anubis 2 3import ( 4 "fmt" 5 "log/slog" 6 "net" 7 "net/http" 8 "strconv" 9 10 "github.com/TecharoHQ/anubis" 11 libanubis "github.com/TecharoHQ/anubis/lib" 12 "github.com/caddyserver/caddy/v2" 13 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 14 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" 15 "github.com/caddyserver/caddy/v2/modules/caddyhttp" 16 "go.uber.org/zap" 17) 18 19func init() { 20 caddy.RegisterModule(AnubisMiddleware{}) 21 httpcaddyfile.RegisterHandlerDirective("anubis", parseCaddyfileHandler) 22 httpcaddyfile.RegisterDirectiveOrder("anubis", httpcaddyfile.Before, "push") 23} 24 25func (AnubisMiddleware) CaddyModule() caddy.ModuleInfo { 26 return caddy.ModuleInfo{ 27 ID: "http.handlers.anubis", 28 New: func() caddy.Module { return new(AnubisMiddleware) }, 29 } 30} 31 32type AnubisMiddleware struct { 33 Options libanubis.Options `json:"options"` 34 PolicyFname string `json:"policy_fname,omitempty"` 35 DefaultDifficulty int `json:"default_difficulty,omitempty"` 36 37 anubis *libanubis.Server 38 log *zap.Logger 39 next caddyhttp.Handler 40 err error 41} 42 43// Interface guards 44var ( 45 _ caddyhttp.MiddlewareHandler = (*AnubisMiddleware)(nil) 46 _ caddyfile.Unmarshaler = (*AnubisMiddleware)(nil) 47 _ caddy.Provisioner = (*AnubisMiddleware)(nil) 48) 49 50func (m *AnubisMiddleware) Provision(ctx caddy.Context) error { 51 m.log = ctx.Logger() 52 53 // TODO: don't set the global slog logger! 54 // currently, anubis does not allow custom loggers (https://github.com/TecharoHQ/anubis/issues/864) 55 sl := ctx.Slogger() 56 slog.SetDefault(sl) 57 58 m.log.Debug("loading anubis policies", zap.String("policy_file", m.PolicyFname), zap.Int("default_difficulty", m.DefaultDifficulty)) 59 policy, err := libanubis.LoadPoliciesOrDefault(ctx, m.PolicyFname, m.DefaultDifficulty) 60 if err != nil { 61 return fmt.Errorf("failed to load anubis policies from '%s': %w", m.PolicyFname, err) 62 } 63 64 m.Options.Next = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 65 if m.err = m.next.ServeHTTP(w, r); err != nil { 66 m.log.Debug("received error from next handler", zap.Error(err)) 67 } 68 }) 69 m.Options.Policy = policy 70 m.anubis, err = libanubis.New(m.Options) 71 72 if err != nil { 73 return err 74 } 75 76 return nil 77} 78 79func (m *AnubisMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { 80 remoteHost, _, err := net.SplitHostPort(r.RemoteAddr) 81 if err != nil { 82 return err 83 } 84 r.Header.Set("X-Real-Ip", remoteHost) 85 86 m.next = next 87 m.err = nil 88 89 m.anubis.ServeHTTP(w, r) 90 if m.err != nil { 91 return m.err 92 } 93 94 return nil 95} 96 97func (m *AnubisMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 98 d.Next() 99 100 m.DefaultDifficulty = anubis.DefaultDifficulty 101 m.Options.CookieExpiration = anubis.CookieDefaultExpirationTime 102 m.Options.CookieSecure = true // TODO: set this only on https upstreams 103 104 for nesting := d.Nesting(); d.NextBlock(nesting); { 105 var err error 106 107 switch d.Val() { 108 case "difficulty": 109 if !d.Next() { 110 return d.ArgErr() 111 } 112 m.DefaultDifficulty, err = strconv.Atoi(d.Val()) 113 if err != nil { 114 return d.WrapErr(err) 115 } 116 case "policy_fname": 117 if !d.Next() { 118 return d.ArgErr() 119 } 120 m.PolicyFname = d.Val() 121 } 122 } // anubis options 123 124 if d.NextArg() { 125 return d.ArgErr() 126 } // too many args 127 128 return nil 129} 130 131func parseCaddyfileHandler(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { 132 var m AnubisMiddleware 133 err := m.UnmarshalCaddyfile(h.Dispenser) 134 return &m, err 135}