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}
41
42// Interface guards
43var (
44 _ caddyhttp.MiddlewareHandler = (*AnubisMiddleware)(nil)
45 _ caddyfile.Unmarshaler = (*AnubisMiddleware)(nil)
46 _ caddy.Provisioner = (*AnubisMiddleware)(nil)
47)
48
49func (m *AnubisMiddleware) Provision(ctx caddy.Context) error {
50 m.log = ctx.Logger()
51
52 // TODO: don't set the global slog logger!
53 // currently, anubis does not allow custom loggers (https://github.com/TecharoHQ/anubis/issues/864)
54 sl := ctx.Slogger()
55 slog.SetDefault(sl)
56
57 m.log.Debug("loading anubis policies", zap.String("policy_file", m.PolicyFname), zap.Int("default_difficulty", m.DefaultDifficulty))
58 policy, err := libanubis.LoadPoliciesOrDefault(ctx, m.PolicyFname, m.DefaultDifficulty)
59 if err != nil {
60 return fmt.Errorf("failed to load anubis policies from '%s': %w", m.PolicyFname, err)
61 }
62
63 m.Options.Next = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
64 if err := m.next.ServeHTTP(w, r); err != nil {
65 m.log.Error("error from next handler", zap.Error(err))
66 }
67 })
68 m.Options.Policy = policy
69 m.anubis, err = libanubis.New(m.Options)
70
71 if err != nil {
72 return err
73 }
74
75 return nil
76}
77
78func (m *AnubisMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
79 remoteHost, _, err := net.SplitHostPort(r.RemoteAddr)
80 if err != nil {
81 return err
82 }
83 r.Header.Set("X-Real-Ip", remoteHost)
84
85 m.next = next
86 m.anubis.ServeHTTP(w, r)
87
88 return nil
89}
90
91func (m *AnubisMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
92 d.Next()
93
94 m.DefaultDifficulty = anubis.DefaultDifficulty
95 m.Options.CookieExpiration = anubis.CookieDefaultExpirationTime
96 m.Options.CookieSecure = true // TODO: set this only on https upstreams
97
98 for nesting := d.Nesting(); d.NextBlock(nesting); {
99 var err error
100
101 switch d.Val() {
102 case "difficulty":
103 if !d.Next() {
104 return d.ArgErr()
105 }
106 m.DefaultDifficulty, err = strconv.Atoi(d.Val())
107 if err != nil {
108 return d.WrapErr(err)
109 }
110 case "policy_fname":
111 if !d.Next() {
112 return d.ArgErr()
113 }
114 m.PolicyFname = d.Val()
115 }
116 } // anubis options
117
118 if d.NextArg() {
119 return d.ArgErr()
120 } // too many args
121
122 return nil
123}
124
125func parseCaddyfileHandler(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
126 var m AnubisMiddleware
127 err := m.UnmarshalCaddyfile(h.Dispenser)
128 return &m, err
129}