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}