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}