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}