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