Anubis module for Caddy
1package caddy_anubis
2
3import (
4 "encoding/hex"
5 "fmt"
6 "net"
7 "net/http"
8 "os"
9 "strconv"
10 "time"
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,omitempty"`
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}
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(m)
53 m.log.Debug("provisioning anubis middleware")
54
55 if m.DefaultDifficulty < 1 {
56 m.log.Debug("default difficulty unset or invalid, using default", zap.Int("default", anubis.DefaultDifficulty))
57 m.DefaultDifficulty = anubis.DefaultDifficulty
58 }
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(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 err := m.next.ServeHTTP(w, r); err != nil {
68 m.log.Error("error from next handler", zap.Error(err))
69 }
70 })
71 m.Options.Policy = policy
72 m.anubis, err = libanubis.New(m.Options)
73
74 if err != nil {
75 return err
76 }
77
78 return nil
79}
80
81func (m *AnubisMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
82 remoteHost, _, err := net.SplitHostPort(r.RemoteAddr)
83 if err != nil {
84 return err
85 }
86 r.Header.Set("X-Real-Ip", remoteHost)
87
88 m.next = next
89 m.anubis.ServeHTTP(w, r)
90
91 return nil
92}
93
94func (m *AnubisMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
95 d.Next()
96
97 m.DefaultDifficulty = anubis.DefaultDifficulty
98
99 for nesting := d.Nesting(); d.NextBlock(nesting); {
100 var err error
101 switch d.Val() {
102 // Numerical flags
103 case "difficulty":
104 if !d.NextArg() {
105 return d.ArgErr()
106 }
107 if m.DefaultDifficulty, err = strconv.Atoi(d.Val()); err != nil {
108 return d.WrapErr(err)
109 }
110 case "og_expiry_time":
111 if !d.NextArg() {
112 return d.ArgErr()
113 }
114 if m.Options.OGTimeToLive, err = time.ParseDuration(d.Val()); err != nil {
115 return d.WrapErr(err)
116 }
117
118 // Boolean flags
119 case "serve_robots_txt":
120 if d.NextArg() {
121 return d.ArgErr()
122 }
123 m.Options.ServeRobotsTXT = true
124 case "cookie_partitioned":
125 if d.NextArg() {
126 return d.ArgErr()
127 }
128 m.Options.CookiePartitioned = true
129 case "og_passthrough":
130 if d.NextArg() {
131 return d.ArgErr()
132 }
133 m.Options.OGPassthrough = true
134
135 // String flags
136 case "policy_fname":
137 if !d.NextArg() {
138 return d.ArgErr()
139 }
140 m.PolicyFname = d.Val()
141 case "webmaster_email":
142 if !d.NextArg() {
143 return d.ArgErr()
144 }
145 m.Options.WebmasterEmail = d.Val()
146 case "cookie_domain":
147 if !d.NextArg() {
148 return d.ArgErr()
149 }
150 m.Options.CookieDomain = d.Val()
151 case "cookie_name":
152 if !d.NextArg() {
153 return d.ArgErr()
154 }
155 m.Options.CookieName = d.Val()
156 case "target":
157 if !d.NextArg() {
158 return d.ArgErr()
159 }
160 m.Options.Target = d.Val()
161 case "private_key":
162 if !d.NextArg() {
163 return d.ArgErr()
164 }
165 if m.Options.PrivateKey, err = hex.DecodeString(d.Val()); err != nil {
166 return d.WrapErr(err)
167 }
168 case "private_key_file":
169 if !d.NextArg() {
170 return d.ArgErr()
171 }
172 hexFile, err := os.ReadFile(d.Val())
173 if err != nil {
174 return d.WrapErr(err)
175 }
176 if _, err = hex.Decode([]byte(m.Options.PrivateKey), hexFile); err != nil {
177 return d.WrapErr(err)
178 }
179 default:
180 return d.SyntaxErr("unknown directive")
181 }
182 } // anubis options
183
184 if d.NextArg() {
185 return d.ArgErr()
186 } // too many args
187
188 return nil
189}
190
191func parseCaddyfileHandler(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
192 var m AnubisMiddleware
193 err := m.UnmarshalCaddyfile(h.Dispenser)
194 return &m, err
195}