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