Anubis module for Caddy
1package caddy_anubis
2
3import (
4 "crypto/ed25519"
5 "encoding/hex"
6 "fmt"
7 "net"
8 "net/http"
9 "strconv"
10
11 "github.com/TecharoHQ/anubis"
12 libanubis "github.com/TecharoHQ/anubis/lib"
13 "github.com/caddyserver/caddy/v2"
14 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
15 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
16 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
17 "go.uber.org/zap"
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 err error
42}
43
44// Interface guards
45var (
46 _ caddyhttp.MiddlewareHandler = (*AnubisMiddleware)(nil)
47 _ caddyfile.Unmarshaler = (*AnubisMiddleware)(nil)
48 _ caddy.Provisioner = (*AnubisMiddleware)(nil)
49)
50
51func (m *AnubisMiddleware) Provision(ctx caddy.Context) error {
52 m.log = ctx.Logger()
53 m.Options.Logger = ctx.Slogger()
54
55 m.log.Debug("loading anubis policies", zap.String("policy_file", m.PolicyFname), zap.Int("default_difficulty", m.DefaultDifficulty))
56 policy, err := libanubis.LoadPoliciesOrDefault(ctx, m.PolicyFname, m.DefaultDifficulty)
57 if err != nil {
58 return fmt.Errorf("failed to load anubis policies from '%s': %w", m.PolicyFname, err)
59 }
60
61 m.Options.Next = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
62 if m.err = m.next.ServeHTTP(w, r); err != nil {
63 m.log.Debug("received error from next handler", zap.Error(err))
64 }
65 })
66 m.Options.Policy = policy
67 m.anubis, err = libanubis.New(m.Options)
68 if err != nil {
69 return err
70 }
71
72 return nil
73}
74
75func (m *AnubisMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
76 remoteHost, _, err := net.SplitHostPort(r.RemoteAddr)
77 if err != nil {
78 return err
79 }
80 r.Header.Set("X-Real-Ip", remoteHost)
81 r.Header.Set("X-Http-Version", r.Proto)
82
83 m.next = next
84 m.err = nil
85
86 m.anubis.ServeHTTP(w, r)
87 if m.err != nil {
88 return m.err
89 }
90
91 return nil
92}
93
94func (m *AnubisMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
95 d.Next()
96
97 m.DefaultDifficulty = anubis.DefaultDifficulty
98 m.Options.CookieExpiration = anubis.CookieDefaultExpirationTime
99 m.Options.CookieSecure = true
100
101 for nesting := d.Nesting(); d.NextBlock(nesting); {
102 var err error
103
104 switch d.Val() {
105 case "difficulty":
106 if !d.Next() {
107 return d.ArgErr()
108 }
109 m.DefaultDifficulty, err = strconv.Atoi(d.Val())
110 if err != nil {
111 return d.WrapErr(err)
112 }
113 case "policy_fname":
114 if !d.Next() {
115 return d.ArgErr()
116 }
117 m.PolicyFname = d.Val()
118 case "private_key":
119 if !d.Next() {
120 return d.ArgErr()
121 }
122 seed, err := hex.DecodeString(d.Val())
123 if err != nil {
124 return d.WrapErr(err)
125 }
126 m.Options.ED25519PrivateKey = ed25519.NewKeyFromSeed(seed)
127 }
128 } // anubis options
129
130 if d.NextArg() {
131 return d.ArgErr()
132 } // too many args
133
134 return nil
135}
136
137func parseCaddyfileHandler(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
138 var m AnubisMiddleware
139 err := m.UnmarshalCaddyfile(h.Dispenser)
140 return &m, err
141}