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}