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