@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 391 lines 11.1 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package acmeserver
16
17 import (
18 "context"
19 "errors"
20 "fmt"
21 weakrand "math/rand/v2"
22 "net"
23 "net/http"
24 "os"
25 "path/filepath"
26 "regexp"
27 "strings"
28 "time"
29
30 "github.com/go-chi/chi/v5"
31 "github.com/smallstep/certificates/acme"
32 "github.com/smallstep/certificates/acme/api"
33 acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql"
34 "github.com/smallstep/certificates/authority"
35 "github.com/smallstep/certificates/authority/provisioner"
36 "github.com/smallstep/certificates/db"
37 "github.com/smallstep/nosql"
38 bolterrors "go.etcd.io/bbolt/errors"
39 "go.uber.org/zap"
40 "go.uber.org/zap/zapcore"
41
42 "github.com/caddyserver/caddy/v2"
43 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
44 "github.com/caddyserver/caddy/v2/modules/caddypki"
45 "github.com/caddyserver/caddy/v2/modules/caddytls"
46 )
47
48 func init() {
49 caddy.RegisterModule(Handler{})
50 }
51
52 // Handler is an ACME server handler.
53 type Handler struct {
54 // The ID of the CA to use for signing. This refers to
55 // the ID given to the CA in the `pki` app. If omitted,
56 // the default ID is "local".
57 CA string `json:"ca,omitempty"`
58
59 // The lifetime for issued certificates
60 Lifetime caddy.Duration `json:"lifetime,omitempty"`
61
62 // The hostname or IP address by which ACME clients
63 // will access the server. This is used to populate
64 // the ACME directory endpoint. If not set, the Host
65 // header of the request will be used.
66 // COMPATIBILITY NOTE / TODO: This property may go away in the
67 // future. Do not rely on this property long-term; check release notes.
68 Host string `json:"host,omitempty"`
69
70 // The path prefix under which to serve all ACME
71 // endpoints. All other requests will not be served
72 // by this handler and will be passed through to
73 // the next one. Default: "/acme/".
74 // COMPATIBILITY NOTE / TODO: This property may go away in the
75 // future, as it is currently only required due to
76 // limitations in the underlying library. Do not rely
77 // on this property long-term; check release notes.
78 PathPrefix string `json:"path_prefix,omitempty"`
79
80 // If true, the CA's root will be the issuer instead of
81 // the intermediate. This is NOT recommended and should
82 // only be used when devices/clients do not properly
83 // validate certificate chains. EXPERIMENTAL: Might be
84 // changed or removed in the future.
85 SignWithRoot bool `json:"sign_with_root,omitempty"`
86
87 // The addresses of DNS resolvers to use when looking up
88 // the TXT records for solving DNS challenges.
89 // It accepts [network addresses](/docs/conventions#network-addresses)
90 // with port range of only 1. If the host is an IP address,
91 // it will be dialed directly to resolve the upstream server.
92 // If the host is not an IP address, the addresses are resolved
93 // using the [name resolution convention](https://golang.org/pkg/net/#hdr-Name_Resolution)
94 // of the Go standard library. If the array contains more
95 // than 1 resolver address, one is chosen at random.
96 Resolvers []string `json:"resolvers,omitempty"`
97
98 // Specify the set of enabled ACME challenges. An empty or absent value
99 // means all challenges are enabled. Accepted values are:
100 // "http-01", "dns-01", "tls-alpn-01"
101 Challenges ACMEChallenges `json:"challenges,omitempty" `
102
103 // The policy to use for issuing certificates
104 Policy *Policy `json:"policy,omitempty"`
105
106 logger *zap.Logger
107 resolvers []caddy.NetworkAddress
108 ctx caddy.Context
109
110 acmeDB acme.DB
111 acmeAuth *authority.Authority
112 acmeClient acme.Client
113 acmeLinker acme.Linker
114 acmeEndpoints http.Handler
115 }
116
117 // CaddyModule returns the Caddy module information.
118 func (Handler) CaddyModule() caddy.ModuleInfo {
119 return caddy.ModuleInfo{
120 ID: "http.handlers.acme_server",
121 New: func() caddy.Module { return new(Handler) },
122 }
123 }
124
125 // Provision sets up the ACME server handler.
126 func (ash *Handler) Provision(ctx caddy.Context) error {
127 ash.ctx = ctx
128 ash.logger = ctx.Logger()
129
130 // set some defaults
131 if ash.CA == "" {
132 ash.CA = caddypki.DefaultCAID
133 }
134 if ash.PathPrefix == "" {
135 ash.PathPrefix = defaultPathPrefix
136 }
137 if ash.Lifetime == 0 {
138 ash.Lifetime = caddy.Duration(12 * time.Hour)
139 }
140 if len(ash.Challenges) > 0 {
141 if err := ash.Challenges.validate(); err != nil {
142 return err
143 }
144 }
145
146 ash.warnIfPolicyAllowsAll()
147
148 // get a reference to the configured CA
149 appModule, err := ctx.App("pki")
150 if err != nil {
151 return err
152 }
153 pkiApp := appModule.(*caddypki.PKI)
154 ca, err := pkiApp.GetCA(ctx, ash.CA)
155 if err != nil {
156 return err
157 }
158
159 // make sure leaf cert lifetime is less than the intermediate cert lifetime. this check only
160 // applies for caddy-managed intermediate certificates
161 if ca.Intermediate == nil && ash.Lifetime >= ca.IntermediateLifetime {
162 return fmt.Errorf("certificate lifetime (%s) should be less than intermediate certificate lifetime (%s)", time.Duration(ash.Lifetime), time.Duration(ca.IntermediateLifetime))
163 }
164
165 database, err := ash.openDatabase()
166 if err != nil {
167 return err
168 }
169
170 authorityConfig := caddypki.AuthorityConfig{
171 SignWithRoot: ash.SignWithRoot,
172 AuthConfig: &authority.AuthConfig{
173 Provisioners: provisioner.List{
174 &provisioner.ACME{
175 Name: ash.CA,
176 Challenges: ash.Challenges.toSmallstepType(),
177 Options: &provisioner.Options{
178 X509: ash.Policy.normalizeRules(),
179 },
180 Type: provisioner.TypeACME.String(),
181 Claims: &provisioner.Claims{
182 MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute},
183 MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour * 365},
184 DefaultTLSDur: &provisioner.Duration{Duration: time.Duration(ash.Lifetime)},
185 },
186 },
187 },
188 },
189 DB: database,
190 }
191
192 ash.acmeAuth, err = ca.NewAuthority(authorityConfig)
193 if err != nil {
194 return err
195 }
196
197 ash.acmeDB, err = acmeNoSQL.New(ash.acmeAuth.GetDatabase().(nosql.DB))
198 if err != nil {
199 return fmt.Errorf("configuring ACME DB: %v", err)
200 }
201
202 ash.acmeClient, err = ash.makeClient()
203 if err != nil {
204 return err
205 }
206
207 ash.acmeLinker = acme.NewLinker(
208 ash.Host,
209 strings.Trim(ash.PathPrefix, "/"),
210 )
211
212 // extract its http.Handler so we can use it directly
213 r := chi.NewRouter()
214 r.Route(ash.PathPrefix, func(r chi.Router) {
215 api.Route(r)
216 })
217 ash.acmeEndpoints = r
218
219 return nil
220 }
221
222 func (ash *Handler) warnIfPolicyAllowsAll() {
223 allow := ash.Policy.normalizeAllowRules()
224 deny := ash.Policy.normalizeDenyRules()
225 if allow != nil || deny != nil {
226 return
227 }
228
229 allowWildcardNames := ash.Policy != nil && ash.Policy.AllowWildcardNames
230 ash.logger.Warn(
231 "acme_server policy has no allow/deny rules; order identifiers are unrestricted (allow-all)",
232 zap.String("ca", ash.CA),
233 zap.Bool("allow_wildcard_names", allowWildcardNames),
234 )
235 }
236
237 func (ash Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
238 if strings.HasPrefix(r.URL.Path, ash.PathPrefix) {
239 acmeCtx := acme.NewContext(
240 r.Context(),
241 ash.acmeDB,
242 ash.acmeClient,
243 ash.acmeLinker,
244 nil,
245 )
246 acmeCtx = authority.NewContext(acmeCtx, ash.acmeAuth)
247 r = r.WithContext(acmeCtx)
248
249 ash.acmeEndpoints.ServeHTTP(w, r)
250 return nil
251 }
252 return next.ServeHTTP(w, r)
253 }
254
255 func (ash Handler) getDatabaseKey() string {
256 key := ash.CA
257 key = strings.ToLower(key)
258 key = strings.TrimSpace(key)
259 return keyCleaner.ReplaceAllLiteralString(key, "")
260 }
261
262 // Cleanup implements caddy.CleanerUpper and closes any idle databases.
263 func (ash Handler) Cleanup() error {
264 key := ash.getDatabaseKey()
265 deleted, err := databasePool.Delete(key)
266 if deleted {
267 if c := ash.logger.Check(zapcore.DebugLevel, "unloading unused CA database"); c != nil {
268 c.Write(zap.String("db_key", key))
269 }
270 }
271 if err != nil {
272 if c := ash.logger.Check(zapcore.ErrorLevel, "closing CA database"); c != nil {
273 c.Write(zap.String("db_key", key), zap.Error(err))
274 }
275 }
276 return err
277 }
278
279 func (ash Handler) openDatabase() (*db.AuthDB, error) {
280 key := ash.getDatabaseKey()
281 database, loaded, err := databasePool.LoadOrNew(key, func() (caddy.Destructor, error) {
282 dbFolder := filepath.Join(caddy.AppDataDir(), "acme_server", key)
283 dbPath := filepath.Join(dbFolder, "db")
284
285 err := os.MkdirAll(dbFolder, 0o755)
286 if err != nil {
287 return nil, fmt.Errorf("making folder for CA database: %v", err)
288 }
289
290 dbConfig := &db.Config{
291 Type: "bbolt",
292 DataSource: dbPath,
293 }
294 database, err := db.New(dbConfig)
295 if errors.Is(err, bolterrors.ErrTimeout) {
296 // bbolt holds an exclusive file lock, so something else has this database open
297 err = fmt.Errorf("%w: CA database %s is already locked; this is usually another running Caddy instance, in which case use 'caddy reload' to apply config changes to it", err, dbPath)
298 }
299 return databaseCloser{&database}, err
300 })
301
302 if loaded {
303 if c := ash.logger.Check(zapcore.DebugLevel, "loaded preexisting CA database"); c != nil {
304 c.Write(zap.String("db_key", key))
305 }
306 }
307
308 return database.(databaseCloser).DB, err
309 }
310
311 // makeClient creates an ACME client which will use a custom
312 // resolver instead of net.DefaultResolver.
313 func (ash Handler) makeClient() (acme.Client, error) {
314 // If no local resolvers are configured, check for global resolvers from TLS app
315 resolversToUse := ash.Resolvers
316 if len(resolversToUse) == 0 {
317 tlsAppIface, err := ash.ctx.App("tls")
318 if err == nil {
319 tlsApp := tlsAppIface.(*caddytls.TLS)
320 if len(tlsApp.Resolvers) > 0 {
321 resolversToUse = tlsApp.Resolvers
322 }
323 }
324 }
325
326 for _, v := range resolversToUse {
327 addr, err := caddy.ParseNetworkAddressWithDefaults(v, "udp", 53)
328 if err != nil {
329 return nil, err
330 }
331 if addr.PortRangeSize() != 1 {
332 return nil, fmt.Errorf("resolver address must have exactly one address; cannot call %v", addr)
333 }
334 ash.resolvers = append(ash.resolvers, addr)
335 }
336
337 var resolver *net.Resolver
338 if len(ash.resolvers) != 0 {
339 dialer := &net.Dialer{
340 Timeout: 2 * time.Second,
341 }
342 resolver = &net.Resolver{
343 PreferGo: true,
344 Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
345 //nolint:gosec
346 addr := ash.resolvers[weakrand.IntN(len(ash.resolvers))]
347 return dialer.DialContext(ctx, addr.Network, addr.JoinHostPort(0))
348 },
349 }
350 } else {
351 resolver = net.DefaultResolver
352 }
353
354 return resolverClient{
355 Client: acme.NewClient(),
356 resolver: resolver,
357 ctx: ash.ctx,
358 }, nil
359 }
360
361 type resolverClient struct {
362 acme.Client
363
364 resolver *net.Resolver
365 ctx context.Context
366 }
367
368 func (c resolverClient) LookupTxt(name string) ([]string, error) {
369 return c.resolver.LookupTXT(c.ctx, name)
370 }
371
372 const defaultPathPrefix = "/acme/"
373
374 var (
375 keyCleaner = regexp.MustCompile(`[^\w.-_]`)
376 databasePool = caddy.NewUsagePool()
377 )
378
379 type databaseCloser struct {
380 DB *db.AuthDB
381 }
382
383 func (closer databaseCloser) Destruct() error {
384 return (*closer.DB).Shutdown()
385 }
386
387 // Interface guards
388 var (
389 _ caddyhttp.MiddlewareHandler = (*Handler)(nil)
390 _ caddy.Provisioner = (*Handler)(nil)
391 )