@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 10, 22:30 UTC

master
go 1,529 lines 49.3 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 caddy
16
17 import (
18 "bytes"
19 "context"
20 "crypto"
21 "crypto/tls"
22 "crypto/x509"
23 "encoding/base64"
24 "encoding/json"
25 "errors"
26 "expvar"
27 "fmt"
28 "hash"
29 "io"
30 "net"
31 "net/http"
32 "net/http/pprof"
33 "net/url"
34 "os"
35 "path"
36 "regexp"
37 "slices"
38 "strconv"
39 "strings"
40 "sync"
41 "time"
42
43 "github.com/caddyserver/certmagic"
44 "github.com/cespare/xxhash/v2"
45 "github.com/prometheus/client_golang/prometheus"
46 "go.uber.org/zap"
47 "go.uber.org/zap/zapcore"
48 "golang.org/x/net/http/httpguts"
49
50 "github.com/caddyserver/caddy/v2/caddyconfig/warning"
51 "github.com/caddyserver/caddy/v2/internal"
52 )
53
54 // testCertMagicStorageOverride is a package-level test hook. Tests may set
55 // this variable to provide a temporary certmagic.Storage so that cert
56 // management in tests does not hit the real default storage on disk.
57 // This must NOT be set in production code.
58 var testCertMagicStorageOverride certmagic.Storage
59
60 func init() {
61 // The hard-coded default `DefaultAdminListen` can be overridden
62 // by setting the `CADDY_ADMIN` environment variable.
63 // The environment variable may be used by packagers to change
64 // the default admin address to something more appropriate for
65 // that platform. See #5317 for discussion.
66 if env, exists := os.LookupEnv("CADDY_ADMIN"); exists {
67 DefaultAdminListen = env
68 }
69 }
70
71 // AdminConfig configures Caddy's API endpoint, which is used
72 // to manage Caddy while it is running.
73 type AdminConfig struct {
74 // If true, the admin endpoint will be completely disabled.
75 // Note that this makes any runtime changes to the config
76 // impossible, since the interface to do so is through the
77 // admin endpoint.
78 Disabled bool `json:"disabled,omitempty"`
79
80 // The address to which the admin endpoint's listener should
81 // bind itself. Can be any single network address that can be
82 // parsed by Caddy. Accepts placeholders.
83 // Default: the value of the `CADDY_ADMIN` environment variable,
84 // or `localhost:2019` otherwise.
85 //
86 // Remember: When changing this value through a config reload,
87 // be sure to use the `--address` CLI flag to specify the current
88 // admin address if the currently-running admin endpoint is not
89 // the default address.
90 Listen string `json:"listen,omitempty"`
91
92 // If true, CORS headers will be emitted, and requests to the
93 // API will be rejected if their `Host` and `Origin` headers
94 // do not match the expected value(s). Use `origins` to
95 // customize which origins/hosts are allowed. If `origins` is
96 // not set, the listen address is the only value allowed by
97 // default. Enforced only on local (plaintext) endpoint.
98 EnforceOrigin bool `json:"enforce_origin,omitempty"`
99
100 // The list of allowed origins/hosts for API requests. Only needed
101 // if accessing the admin endpoint from a host different from the
102 // socket's network interface or if `enforce_origin` is true. If not
103 // set, the listener address will be the default value. If set but
104 // empty, no origins will be allowed. Enforced only on local
105 // (plaintext) endpoint.
106 Origins []string `json:"origins,omitempty"`
107
108 // Options pertaining to configuration management.
109 Config *ConfigSettings `json:"config,omitempty"`
110
111 // Options that establish this server's identity. Identity refers to
112 // credentials which can be used to uniquely identify and authenticate
113 // this server instance. This is required if remote administration is
114 // enabled (but does not require remote administration to be enabled).
115 // Default: no identity management.
116 Identity *IdentityConfig `json:"identity,omitempty"`
117
118 // Options pertaining to remote administration. By default, remote
119 // administration is disabled. If enabled, identity management must
120 // also be configured, as that is how the endpoint is secured.
121 // See the neighboring "identity" object.
122 //
123 // EXPERIMENTAL: This feature is subject to change.
124 Remote *RemoteAdmin `json:"remote,omitempty"`
125 }
126
127 // ConfigSettings configures the management of configuration.
128 type ConfigSettings struct {
129 // Whether to keep a copy of the active config on disk. Default is true.
130 // Note that "pulled" dynamic configs (using the neighboring "load" module)
131 // are not persisted; only configs that are pushed to Caddy get persisted.
132 Persist *bool `json:"persist,omitempty"`
133
134 // Loads a new configuration. This is helpful if your configs are
135 // managed elsewhere and you want Caddy to pull its config dynamically
136 // when it starts. The pulled config completely replaces the current
137 // one, just like any other config load. It is an error if a pulled
138 // config is configured to pull another config without a load_delay,
139 // as this creates a tight loop.
140 //
141 // EXPERIMENTAL: Subject to change.
142 LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
143
144 // The duration after which to load config. If set, config will be pulled
145 // from the config loader after this duration. A delay is required if a
146 // dynamically-loaded config is configured to load yet another config. To
147 // load configs on a regular interval, ensure this value is set the same
148 // on all loaded configs; it can also be variable if needed, and to stop
149 // the loop, simply remove dynamic config loading from the next-loaded
150 // config.
151 //
152 // EXPERIMENTAL: Subject to change.
153 LoadDelay Duration `json:"load_delay,omitempty"`
154 }
155
156 // IdentityConfig configures management of this server's identity. An identity
157 // consists of credentials that uniquely verify this instance; for example,
158 // TLS certificates (public + private key pairs).
159 type IdentityConfig struct {
160 // List of names or IP addresses which refer to this server.
161 // Certificates will be obtained for these identifiers so
162 // secure TLS connections can be made using them.
163 Identifiers []string `json:"identifiers,omitempty"`
164
165 // Issuers that can provide this admin endpoint its identity
166 // certificate(s). Default: ACME issuers configured for
167 // ZeroSSL and Let's Encrypt. Be sure to change this if you
168 // require credentials for private identifiers.
169 IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
170
171 issuers []certmagic.Issuer
172 }
173
174 // RemoteAdmin enables and configures remote administration. If enabled,
175 // a secure listener enforcing mutual TLS authentication will be started
176 // on a different port from the standard plaintext admin server.
177 //
178 // This endpoint is secured using identity management, which must be
179 // configured separately (because identity management does not depend
180 // on remote administration). See the admin/identity config struct.
181 //
182 // EXPERIMENTAL: Subject to change.
183 type RemoteAdmin struct {
184 // The address on which to start the secure listener. Accepts placeholders.
185 // Default: :2021
186 Listen string `json:"listen,omitempty"`
187
188 // List of access controls for this secure admin endpoint.
189 // This configures TLS mutual authentication (i.e. authorized
190 // client certificates), but also application-layer permissions
191 // like which paths and methods each identity is authorized for.
192 AccessControl []*AdminAccess `json:"access_control,omitempty"`
193 }
194
195 // AdminAccess specifies what permissions an identity or group
196 // of identities are granted.
197 type AdminAccess struct {
198 // Base64-encoded DER certificates containing public keys to accept.
199 // (The contents of PEM certificate blocks are base64-encoded DER.)
200 // Any of these public keys can appear in any part of a verified chain.
201 PublicKeys []string `json:"public_keys,omitempty"`
202
203 // Limits what the associated identities are allowed to do.
204 // If unspecified, all permissions are granted.
205 Permissions []AdminPermissions `json:"permissions,omitempty"`
206
207 publicKeys []crypto.PublicKey
208 }
209
210 // AdminPermissions specifies what kinds of requests are allowed
211 // to be made to the admin endpoint.
212 type AdminPermissions struct {
213 // The API paths allowed. A request path must either equal an
214 // allowed path or be a subpath with a path-segment boundary.
215 Paths []string `json:"paths,omitempty"`
216
217 // The HTTP methods allowed for the given paths.
218 Methods []string `json:"methods,omitempty"`
219 }
220
221 // newAdminHandler reads admin's config and returns an http.Handler suitable
222 // for use in an admin endpoint server, which will be listening on listenAddr.
223 func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) (adminHandler, error) {
224 muxWrap := adminHandler{mux: http.NewServeMux()}
225
226 // secure the local or remote endpoint respectively
227 if remote {
228 muxWrap.remoteControl = admin.Remote
229 } else {
230 // see comment in allowedOrigins() as to why we disable the host check for unix/fd networks
231 muxWrap.enforceHost = !addr.isWildcardInterface() && !addr.IsUnixNetwork() && !addr.IsFdNetwork()
232 muxWrap.allowedOrigins = admin.allowedOrigins(addr)
233 muxWrap.enforceOrigin = admin.EnforceOrigin
234 }
235
236 addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
237 labels := prometheus.Labels{"path": pattern, "handler": handlerLabel}
238 h = instrumentHandlerCounter(
239 adminMetrics.requestCount.MustCurryWith(labels),
240 h,
241 )
242 muxWrap.mux.Handle(pattern, h)
243 }
244 // addRoute just calls muxWrap.mux.Handle after
245 // wrapping the handler with error handling
246 addRoute := func(pattern string, handlerLabel string, h AdminHandler) {
247 wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
248 err := h.ServeHTTP(w, r)
249 if err != nil {
250 labels := prometheus.Labels{
251 "path": pattern,
252 "handler": handlerLabel,
253 "method": strings.ToUpper(r.Method),
254 }
255 adminMetrics.requestErrors.With(labels).Inc()
256 }
257 muxWrap.handleError(w, r, err)
258 })
259 addRouteWithMetrics(pattern, handlerLabel, wrapper)
260 }
261
262 const handlerLabel = "admin"
263
264 // register standard config control endpoints
265 addRoute("/"+rawConfigKey+"/", handlerLabel, AdminHandlerFunc(handleConfig))
266 addRoute("/id/", handlerLabel, AdminHandlerFunc(handleConfigID))
267 addRoute("/stop", handlerLabel, AdminHandlerFunc(handleStop))
268
269 // register debugging endpoints
270 addRouteWithMetrics("/debug/pprof/", handlerLabel, http.HandlerFunc(pprof.Index))
271 addRouteWithMetrics("/debug/pprof/cmdline", handlerLabel, http.HandlerFunc(pprof.Cmdline))
272 addRouteWithMetrics("/debug/pprof/profile", handlerLabel, http.HandlerFunc(pprof.Profile))
273 addRouteWithMetrics("/debug/pprof/symbol", handlerLabel, http.HandlerFunc(pprof.Symbol))
274 addRouteWithMetrics("/debug/pprof/trace", handlerLabel, http.HandlerFunc(pprof.Trace))
275 addRouteWithMetrics("/debug/vars", handlerLabel, expvar.Handler())
276
277 // register third-party module endpoints
278 for _, m := range GetModules("admin.api") {
279 router := m.New().(AdminRouter)
280
281 // provision the router before registering its routes, so
282 // handlers have access to all provisioned state
283 if provisioner, ok := router.(Provisioner); ok {
284 if err := provisioner.Provision(ctx); err != nil {
285 return adminHandler{}, fmt.Errorf("provisioning admin router module %s: %v", m.ID, err)
286 }
287 }
288
289 for _, route := range router.Routes() {
290 addRoute(route.Pattern, handlerLabel, route.Handler)
291 }
292 }
293
294 return muxWrap, nil
295 }
296
297 // allowedOrigins returns a list of origins that are allowed.
298 // If admin.Origins is nil (null), the provided listen address
299 // will be used as the default origin. If admin.Origins is
300 // empty, no origins will be allowed, effectively bricking the
301 // endpoint for non-unix-socket endpoints, but whatever.
302 func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
303 uniqueOrigins := make(map[string]struct{})
304 for _, o := range admin.Origins {
305 uniqueOrigins[o] = struct{}{}
306 }
307 // RFC 2616, Section 14.26:
308 // "A client MUST include a Host header field in all HTTP/1.1 request
309 // messages. If the requested URI does not include an Internet host
310 // name for the service being requested, then the Host header field MUST
311 // be given with an empty value."
312 //
313 // UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
314 // Understandable, but frustrating. See:
315 // https://github.com/golang/go/issues/60374
316 // See also the discussion here:
317 // https://github.com/golang/go/issues/61431
318 //
319 // We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
320 // in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
321 // bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
322 // security checks, the infosec community assures me that it is secure to do
323 // so, because:
324 //
325 // 1) Browsers do not allow access to unix sockets
326 // 2) DNS is irrelevant to unix sockets
327 //
328 // If either of those two statements ever fail to hold true, it is not the
329 // fault of Caddy.
330 //
331 // Thus, we do not fill out allowed origins and do not enforce Host
332 // requirements for unix sockets. Enforcing it leads to confusion and
333 // frustration, when UDS have their own permissions from the OS.
334 // Enforcing host requirements here is effectively security theater,
335 // and a false sense of security.
336 //
337 // See also the discussion in #6832.
338 if admin.Origins == nil && !addr.IsUnixNetwork() && !addr.IsFdNetwork() {
339 if addr.isLoopback() {
340 uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
341 uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
342 uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
343 } else {
344 uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
345 }
346 }
347 allowed := make([]*url.URL, 0, len(uniqueOrigins))
348 for originStr := range uniqueOrigins {
349 var origin *url.URL
350 if strings.Contains(originStr, "://") {
351 var err error
352 origin, err = url.Parse(originStr)
353 if err != nil {
354 continue
355 }
356 origin.Path = ""
357 origin.RawPath = ""
358 origin.Fragment = ""
359 origin.RawFragment = ""
360 origin.RawQuery = ""
361 } else {
362 origin = &url.URL{Host: originStr}
363 }
364 allowed = append(allowed, origin)
365 }
366 return allowed
367 }
368
369 // replaceLocalAdminServer replaces the running local admin server
370 // according to the relevant configuration in cfg. If no configuration
371 // for the admin endpoint exists in cfg, a default one is used, so
372 // that there is always an admin server (unless it is explicitly
373 // configured to be disabled).
374 // Critically note that some elements and functionality of the context
375 // may not be ready, e.g. storage. Tread carefully.
376 func replaceLocalAdminServer(cfg *Config, ctx Context) error {
377 // always* be sure to close down the old admin endpoint
378 // as gracefully as possible, even if the new one is
379 // disabled -- careful to use reference to the current
380 // (old) admin endpoint since it will be different
381 // when the function returns
382 // (* except if the new one fails to start)
383 oldAdminServer := localAdminServer
384 var err error
385 defer func() {
386 // do the shutdown asynchronously so that any
387 // current API request gets a response; this
388 // goroutine may last a few seconds
389 if oldAdminServer != nil && err == nil {
390 go func(oldAdminServer *http.Server) {
391 err := stopAdminServer(oldAdminServer)
392 if err != nil {
393 Log().Named("admin").Error("stopping current admin endpoint", zap.Error(err))
394 }
395 }(oldAdminServer)
396 }
397 }()
398
399 // set a default if admin wasn't otherwise configured
400 if cfg.Admin == nil {
401 cfg.Admin = &AdminConfig{
402 Listen: DefaultAdminListen,
403 }
404 }
405
406 // if new admin endpoint is to be disabled, we're done
407 if cfg.Admin.Disabled {
408 Log().Named("admin").Warn("admin endpoint disabled")
409 return nil
410 }
411
412 // extract a singular listener address
413 addr, err := parseAdminListenAddr(cfg.Admin.Listen, DefaultAdminListen)
414 if err != nil {
415 return err
416 }
417
418 handler, err := cfg.Admin.newAdminHandler(addr, false, ctx)
419 if err != nil {
420 return err
421 }
422
423 ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
424 if err != nil {
425 return err
426 }
427
428 serverMu.Lock()
429 localAdminServer = &http.Server{
430 Addr: addr.String(), // for logging purposes only
431 Handler: handler,
432 ReadTimeout: 10 * time.Second,
433 ReadHeaderTimeout: 5 * time.Second,
434 IdleTimeout: 60 * time.Second,
435 MaxHeaderBytes: 1024 * 64,
436 }
437 serverMu.Unlock()
438
439 adminLogger := Log().Named("admin")
440 go func() {
441 serverMu.Lock()
442 server := localAdminServer
443 serverMu.Unlock()
444 if err := server.Serve(ln.(net.Listener)); !errors.Is(err, http.ErrServerClosed) {
445 adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
446 }
447 }()
448
449 adminLogger.Info("admin endpoint started",
450 zap.String("address", addr.String()),
451 zap.Bool("enforce_origin", cfg.Admin.EnforceOrigin),
452 zap.Array("origins", loggableURLArray(handler.allowedOrigins)))
453
454 if !handler.enforceHost {
455 adminLogger.Warn("admin endpoint on open interface; host checking disabled",
456 zap.String("address", addr.String()))
457 }
458
459 return nil
460 }
461
462 // manageIdentity sets up automated identity management for this server.
463 func manageIdentity(ctx Context, cfg *Config) error {
464 if cfg == nil || cfg.Admin == nil || cfg.Admin.Identity == nil {
465 return nil
466 }
467
468 // set default issuers; this is pretty hacky because we can't
469 // import the caddytls package -- but it works
470 if cfg.Admin.Identity.IssuersRaw == nil {
471 cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
472 json.RawMessage(`{"module": "acme"}`),
473 }
474 }
475
476 // load and provision issuer modules
477 if cfg.Admin.Identity.IssuersRaw != nil {
478 val, err := ctx.LoadModule(cfg.Admin.Identity, "IssuersRaw")
479 if err != nil {
480 return fmt.Errorf("loading identity issuer modules: %s", err)
481 }
482 for _, issVal := range val.([]any) {
483 cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
484 }
485 }
486
487 // we'll make a new cache when we make the CertMagic config, so stop any previous cache
488 if identityCertCache != nil {
489 identityCertCache.Stop()
490 }
491
492 logger := Log().Named("admin.identity")
493 cmCfg := cfg.Admin.Identity.certmagicConfig(logger, true)
494
495 // issuers have circular dependencies with the configs because,
496 // as explained in the caddytls package, they need access to the
497 // correct storage and cache to solve ACME challenges
498 for _, issuer := range cfg.Admin.Identity.issuers {
499 // avoid import cycle with caddytls package, so manually duplicate the interface here, yuck
500 if annoying, ok := issuer.(interface{ SetConfig(cfg *certmagic.Config) }); ok {
501 annoying.SetConfig(cmCfg)
502 }
503 }
504
505 // obtain and renew server identity certificate(s)
506 return cmCfg.ManageAsync(ctx, cfg.Admin.Identity.Identifiers)
507 }
508
509 // replaceRemoteAdminServer replaces the running remote admin server
510 // according to the relevant configuration in cfg. It stops any previous
511 // remote admin server and only starts a new one if configured.
512 func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
513 if cfg == nil {
514 return nil
515 }
516
517 remoteLogger := Log().Named("admin.remote")
518
519 oldAdminServer := remoteAdminServer
520 defer func() {
521 if oldAdminServer != nil {
522 go func(oldAdminServer *http.Server) {
523 err := stopAdminServer(oldAdminServer)
524 if err != nil {
525 Log().Named("admin").Error("stopping current secure admin endpoint", zap.Error(err))
526 }
527 }(oldAdminServer)
528 }
529 }()
530
531 if cfg.Admin == nil || cfg.Admin.Remote == nil {
532 return nil
533 }
534
535 addr, err := parseAdminListenAddr(cfg.Admin.Remote.Listen, DefaultRemoteAdminListen)
536 if err != nil {
537 return err
538 }
539
540 // make the HTTP handler but disable Host/Origin enforcement
541 // because we are using TLS authentication instead
542 handler, err := cfg.Admin.newAdminHandler(addr, true, ctx)
543 if err != nil {
544 return err
545 }
546
547 // create client certificate pool for TLS mutual auth, and extract public keys
548 // so that we can enforce access controls at the application layer
549 clientCertPool := x509.NewCertPool()
550 for i, accessControl := range cfg.Admin.Remote.AccessControl {
551 for j, certBase64 := range accessControl.PublicKeys {
552 cert, err := decodeBase64DERCert(certBase64)
553 if err != nil {
554 return fmt.Errorf("access control %d public key %d: parsing base64 certificate DER: %v", i, j, err)
555 }
556 accessControl.publicKeys = append(accessControl.publicKeys, cert.PublicKey)
557 clientCertPool.AddCert(cert)
558 }
559 for j, perm := range accessControl.Permissions {
560 for _, permPath := range perm.Paths {
561 if permPath == "" || permPath == "/" {
562 continue
563 }
564 cleanPath := path.Clean(permPath)
565 hasCanonicalTrailingSlash := cleanPath != "/" && strings.TrimSuffix(permPath, "/") == cleanPath
566 if cleanPath != permPath && !hasCanonicalTrailingSlash {
567 return fmt.Errorf("access control %d permission %d: path %q is not canonical (did you mean %q?)", i, j, permPath, cleanPath)
568 }
569 }
570 }
571 }
572
573 // create TLS config that will enforce mutual authentication
574 if identityCertCache == nil {
575 return fmt.Errorf("cannot enable remote admin without a certificate cache; configure identity management to initialize a certificate cache")
576 }
577 cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false)
578 tlsConfig := cmCfg.TLSConfig()
579 tlsConfig.NextProtos = nil // this server does not solve ACME challenges
580 tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
581 tlsConfig.ClientCAs = clientCertPool
582
583 // convert logger to stdlib so it can be used by HTTP server
584 serverLogger, err := zap.NewStdLogAt(remoteLogger, zap.DebugLevel)
585 if err != nil {
586 return err
587 }
588
589 serverMu.Lock()
590 // create secure HTTP server
591 remoteAdminServer = &http.Server{
592 Addr: addr.String(), // for logging purposes only
593 Handler: handler,
594 TLSConfig: tlsConfig,
595 ReadTimeout: 10 * time.Second,
596 ReadHeaderTimeout: 5 * time.Second,
597 IdleTimeout: 60 * time.Second,
598 MaxHeaderBytes: 1024 * 64,
599 ErrorLog: serverLogger,
600 }
601 serverMu.Unlock()
602
603 // start listener
604 lnAny, err := addr.Listen(ctx, 0, net.ListenConfig{})
605 if err != nil {
606 return err
607 }
608 ln := lnAny.(net.Listener)
609 ln = tls.NewListener(ln, tlsConfig)
610
611 go func() {
612 serverMu.Lock()
613 server := remoteAdminServer
614 serverMu.Unlock()
615 if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
616 remoteLogger.Error("admin remote server shutdown for unknown reason", zap.Error(err))
617 }
618 }()
619
620 remoteLogger.Info("secure admin remote control endpoint started",
621 zap.String("address", addr.String()))
622
623 return nil
624 }
625
626 func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config {
627 var cmCfg *certmagic.Config
628 if ident == nil {
629 // user might not have configured identity; that's OK, we can still make a
630 // certmagic config, although it'll be mostly useless for remote management
631 ident = new(IdentityConfig)
632 }
633 // Choose storage: prefer the package-level test override when present,
634 // otherwise use the configured DefaultStorage. Tests may set an override
635 // to divert storage into a temporary location. Otherwise, in production
636 // we use the DefaultStorage since we don't want to act as part of a
637 // cluster; this storage is for the server's local identity only.
638 var storage certmagic.Storage
639 if testCertMagicStorageOverride != nil {
640 storage = testCertMagicStorageOverride
641 } else {
642 storage = DefaultStorage
643 }
644 template := certmagic.Config{
645 Storage: storage,
646 Logger: logger,
647 Issuers: ident.issuers,
648 }
649 if makeCache {
650 identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
651 GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
652 return cmCfg, nil
653 },
654 Logger: logger.Named("cache"),
655 })
656 }
657 cmCfg = certmagic.New(identityCertCache, template)
658 return cmCfg
659 }
660
661 // IdentityCredentials returns this instance's configured, managed identity credentials
662 // that can be used in TLS client authentication.
663 func (ctx Context) IdentityCredentials(logger *zap.Logger) ([]tls.Certificate, error) {
664 if ctx.cfg == nil || ctx.cfg.Admin == nil || ctx.cfg.Admin.Identity == nil {
665 return nil, fmt.Errorf("no server identity configured")
666 }
667 ident := ctx.cfg.Admin.Identity
668 if len(ident.Identifiers) == 0 {
669 return nil, fmt.Errorf("no identifiers configured")
670 }
671 if logger == nil {
672 logger = Log()
673 }
674 magic := ident.certmagicConfig(logger, false)
675 return magic.ClientCredentials(ctx, ident.Identifiers)
676 }
677
678 // enforceAccessControls enforces application-layer access controls for r based on remote.
679 // It expects that the TLS server has already established at least one verified chain of
680 // trust, and then looks for a matching, authorized public key that is allowed to access
681 // the defined path(s) using the defined method(s).
682 func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
683 // the same public key may appear in more than one access control entry;
684 // the request is allowed if any entry containing the key permits it
685 var keyRecognized, pathAllowedSomewhere bool
686
687 for _, chain := range r.TLS.VerifiedChains {
688 for _, peerCert := range chain {
689 for _, adminAccess := range remote.AccessControl {
690 for _, allowedKey := range adminAccess.publicKeys {
691 // see if we found a matching public key; the TLS server already verified the chain
692 // so we know the client possesses the associated private key; this handy interface
693 // doesn't appear to be defined anywhere in the std lib, but was implemented here:
694 // https://github.com/golang/go/commit/b5f2c0f50297fa5cd14af668ddd7fd923626cf8c
695 comparer, ok := peerCert.PublicKey.(interface{ Equal(crypto.PublicKey) bool })
696 if !ok || !comparer.Equal(allowedKey) {
697 continue
698 }
699 keyRecognized = true
700
701 // key recognized; make sure its HTTP request is permitted
702 // by at least one of the permission entries (if none are
703 // configured, all permissions are granted)
704 if len(adminAccess.Permissions) == 0 {
705 return nil
706 }
707 for _, accessPerm := range adminAccess.Permissions {
708 // verify path
709 pathFound := accessPerm.Paths == nil
710 for _, allowedPath := range accessPerm.Paths {
711 if adminPathAllowed(r.URL.Path, allowedPath) {
712 pathFound = true
713 break
714 }
715 }
716 if !pathFound {
717 continue
718 }
719 pathAllowedSomewhere = true
720
721 // verify method
722 if accessPerm.Methods == nil || slices.Contains(accessPerm.Methods, r.Method) {
723 // public key authorized, method and path allowed
724 return nil
725 }
726 }
727
728 // this entry does not permit the request; other entries
729 // containing the same key may
730 break
731 }
732 }
733 }
734 }
735
736 if keyRecognized {
737 // no entry allows this request; if the path is allowed by
738 // some entry, the method is what's missing
739 if pathAllowedSomewhere {
740 return APIError{
741 HTTPStatus: http.StatusForbidden,
742 Message: "not authorized to use this method",
743 }
744 }
745 return APIError{
746 HTTPStatus: http.StatusForbidden,
747 Message: "not authorized to access this path",
748 }
749 }
750
751 // in theory, this should never happen; with an unverified chain, the TLS server
752 // should not accept the connection in the first place, and the acceptable cert
753 // pool is configured using the same list of public keys we verify against
754 return APIError{
755 HTTPStatus: http.StatusUnauthorized,
756 Message: "client identity not authorized",
757 }
758 }
759
760 func adminPathAllowed(reqPath, allowedPath string) bool {
761 reqPathHadTrailingSlash := strings.HasSuffix(reqPath, "/")
762 reqPath = path.Clean(reqPath)
763 if allowedPath == "" {
764 return true
765 }
766 allowedPathHadTrailingSlash := allowedPath != "/" && strings.HasSuffix(allowedPath, "/")
767 allowedPath = path.Clean(allowedPath)
768 if allowedPath == "/" {
769 return true
770 }
771 if reqPath == allowedPath {
772 return !allowedPathHadTrailingSlash || reqPathHadTrailingSlash
773 }
774 return strings.HasPrefix(reqPath, allowedPath+"/")
775 }
776
777 func stopAdminServer(srv *http.Server) error {
778 if srv == nil {
779 return fmt.Errorf("no admin server")
780 }
781 timeout := 10 * time.Second
782 ctx, cancel := context.WithTimeoutCause(context.Background(), timeout, fmt.Errorf("stopping admin server: %ds timeout", int(timeout.Seconds())))
783 defer cancel()
784 err := srv.Shutdown(ctx)
785 if err != nil {
786 if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
787 err = cause
788 }
789 return fmt.Errorf("shutting down admin server: %v", err)
790 }
791 Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
792 return nil
793 }
794
795 // AdminRouter is a type which can return routes for the admin API.
796 type AdminRouter interface {
797 Routes() []AdminRoute
798 }
799
800 // AdminRoute represents a route for the admin endpoint.
801 type AdminRoute struct {
802 Pattern string
803 Handler AdminHandler
804 }
805
806 type adminHandler struct {
807 mux *http.ServeMux
808
809 // security for local/plaintext endpoint
810 enforceOrigin bool
811 enforceHost bool
812 allowedOrigins []*url.URL
813
814 // security for remote/encrypted endpoint
815 remoteControl *RemoteAdmin
816 }
817
818 // ServeHTTP is the external entry point for API requests.
819 // It will only be called once per request.
820 func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
821 ip, port, err := net.SplitHostPort(r.RemoteAddr)
822 if err != nil {
823 ip = r.RemoteAddr
824 port = ""
825 }
826 log := Log().Named("admin.api").With(
827 zap.String("method", r.Method),
828 zap.String("host", r.Host),
829 zap.String("uri", r.RequestURI),
830 zap.String("remote_ip", ip),
831 zap.String("remote_port", port),
832 zap.Object("headers", internal.LoggableHTTPHeader{Header: r.Header}),
833 )
834 if r.TLS != nil {
835 log = log.With(
836 zap.Bool("secure", true),
837 zap.Int("verified_chains", len(r.TLS.VerifiedChains)),
838 )
839 }
840 if r.RequestURI == "/metrics" {
841 log.Debug("received request")
842 } else {
843 log.Info("received request")
844 }
845 h.serveHTTP(w, r)
846 }
847
848 // serveHTTP is the internal entry point for API requests. It may
849 // be called more than once per request, for example if a request
850 // is rewritten (i.e. internal redirect).
851 func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
852 if h.remoteControl != nil {
853 // enforce access controls on secure endpoint
854 if err := h.remoteControl.enforceAccessControls(r); err != nil {
855 h.handleError(w, r, err)
856 return
857 }
858 }
859
860 // common mitigations in browser contexts
861 if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
862 // I've never been able demonstrate a vulnerability myself, but apparently
863 // WebSocket connections originating from browsers aren't subject to CORS
864 // restrictions, so we'll just be on the safe side
865 h.handleError(w, r, APIError{
866 HTTPStatus: http.StatusBadRequest,
867 Err: errors.New("websocket connections aren't allowed"),
868 Message: "WebSocket connections aren't allowed.",
869 })
870 return
871 }
872 if strings.Contains(r.Header.Get("Sec-Fetch-Mode"), "no-cors") {
873 // turns out web pages can just disable the same-origin policy (!???!?)
874 // but at least browsers let us know that's the case, holy heck
875 h.handleError(w, r, APIError{
876 HTTPStatus: http.StatusBadRequest,
877 Err: errors.New("client attempted to make request by disabling same-origin policy using no-cors mode"),
878 Message: "Disabling same-origin restrictions is not allowed.",
879 })
880 return
881 }
882 if r.Header.Get("Origin") == "null" {
883 // bug in Firefox in certain cross-origin situations (yikes?)
884 // (not strictly a security vuln on its own, but it's red flaggy,
885 // since it seems to manifest in cross-origin contexts)
886 h.handleError(w, r, APIError{
887 HTTPStatus: http.StatusBadRequest,
888 Err: errors.New("invalid origin 'null'"),
889 Message: "Buggy browser is sending null Origin header.",
890 })
891 return
892 }
893
894 if h.enforceHost {
895 // DNS rebinding mitigation
896 err := h.checkHost(r)
897 if err != nil {
898 h.handleError(w, r, err)
899 return
900 }
901 }
902
903 _, hasOriginHeader := r.Header["Origin"]
904 _, hasSecHeader := r.Header["Sec-Fetch-Mode"]
905 if h.enforceOrigin || hasOriginHeader || hasSecHeader {
906 // cross-site mitigation
907 origin, err := h.checkOrigin(r)
908 if err != nil {
909 h.handleError(w, r, err)
910 return
911 }
912
913 if r.Method == http.MethodOptions {
914 w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE")
915 w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Cache-Control")
916 w.Header().Set("Access-Control-Allow-Credentials", "true")
917 }
918 w.Header().Set("Access-Control-Allow-Origin", origin)
919 }
920
921 h.mux.ServeHTTP(w, r)
922 }
923
924 func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err error) {
925 if err == nil {
926 return
927 }
928 if err == errInternalRedir {
929 h.serveHTTP(w, r)
930 return
931 }
932
933 apiErr, ok := err.(APIError)
934 if !ok {
935 apiErr = APIError{
936 HTTPStatus: http.StatusInternalServerError,
937 Err: err,
938 }
939 }
940 if apiErr.HTTPStatus == 0 {
941 apiErr.HTTPStatus = http.StatusInternalServerError
942 }
943 if apiErr.Message == "" && apiErr.Err != nil {
944 apiErr.Message = apiErr.Err.Error()
945 }
946
947 Log().Named("admin.api").Error("request error",
948 zap.Error(err),
949 zap.Int("status_code", apiErr.HTTPStatus),
950 )
951
952 w.Header().Set("Content-Type", "application/json")
953 w.WriteHeader(apiErr.HTTPStatus)
954 encErr := json.NewEncoder(w).Encode(apiErr)
955 if encErr != nil {
956 Log().Named("admin.api").Error("failed to encode error response", zap.Error(encErr))
957 }
958 }
959
960 // checkHost returns a handler that wraps next such that
961 // it will only be called if the request's Host header matches
962 // a trustworthy/expected value. This helps to mitigate DNS
963 // rebinding attacks.
964 func (h adminHandler) checkHost(r *http.Request) error {
965 allowed := slices.ContainsFunc(h.allowedOrigins, func(u *url.URL) bool {
966 // Host comparison is case-insensitive per RFC 3986 §3.2.2, same as
967 // the Origin check below; url.Parse does not normalize host case.
968 return strings.EqualFold(r.Host, u.Host)
969 })
970 if !allowed {
971 return APIError{
972 HTTPStatus: http.StatusForbidden,
973 Err: fmt.Errorf("host not allowed: %s", r.Host),
974 }
975 }
976 return nil
977 }
978
979 // checkOrigin ensures that the Origin header, if
980 // set, matches the intended target; prevents arbitrary
981 // sites from issuing requests to our listener. It
982 // returns the origin that was obtained from r.
983 func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
984 originStr, origin := h.getOrigin(r)
985 if origin == nil {
986 return "", APIError{
987 HTTPStatus: http.StatusForbidden,
988 Err: fmt.Errorf("required Origin header is missing or invalid"),
989 }
990 }
991 if !h.originAllowed(origin) {
992 return "", APIError{
993 HTTPStatus: http.StatusForbidden,
994 Err: fmt.Errorf("client is not allowed to access from origin '%s'", originStr),
995 }
996 }
997 return origin.String(), nil
998 }
999
1000 func (h adminHandler) getOrigin(r *http.Request) (string, *url.URL) {
1001 origin := r.Header.Get("Origin")
1002 if origin == "" {
1003 origin = r.Header.Get("Referer")
1004 }
1005 originURL, err := url.Parse(origin)
1006 if err != nil {
1007 return origin, nil
1008 }
1009 originURL.Path = ""
1010 originURL.RawPath = ""
1011 originURL.Fragment = ""
1012 originURL.RawFragment = ""
1013 originURL.RawQuery = ""
1014 return origin, originURL
1015 }
1016
1017 func (h adminHandler) originAllowed(origin *url.URL) bool {
1018 for _, allowedOrigin := range h.allowedOrigins {
1019 if allowedOrigin.Scheme != "" && origin.Scheme != allowedOrigin.Scheme {
1020 continue
1021 }
1022 // Host comparison is case-insensitive per RFC 3986 §3.2.2; url.Parse
1023 // does not normalize host case, so fold it here.
1024 if strings.EqualFold(origin.Host, allowedOrigin.Host) {
1025 return true
1026 }
1027 }
1028 return false
1029 }
1030
1031 // etagHasher returns the hasher we used on the config to both
1032 // produce and verify ETags.
1033 func etagHasher() hash.Hash { return xxhash.New() }
1034
1035 // makeEtag returns an Etag header value (including quotes) for
1036 // the given config path and hash of contents at that path.
1037 func makeEtag(path string, hash hash.Hash) string {
1038 return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil))
1039 }
1040
1041 // This buffer pool is used to keep buffers for
1042 // reading the config file during eTag header generation
1043 var bufferPool = sync.Pool{
1044 New: func() any {
1045 return new(bytes.Buffer)
1046 },
1047 }
1048
1049 func handleConfig(w http.ResponseWriter, r *http.Request) error {
1050 switch r.Method {
1051 case http.MethodGet:
1052 w.Header().Set("Content-Type", "application/json")
1053 hash := etagHasher()
1054
1055 // Read the config into a buffer instead of writing directly to
1056 // the response writer, as we want to set the ETag as the header,
1057 // not the trailer.
1058 buf := bufferPool.Get().(*bytes.Buffer)
1059 buf.Reset()
1060 defer bufferPool.Put(buf)
1061
1062 configWriter := io.MultiWriter(buf, hash)
1063 err := readConfig(r.URL.Path, configWriter)
1064 if err != nil {
1065 return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
1066 }
1067
1068 // we could consider setting up a sync.Pool for the summed
1069 // hashes to reduce GC pressure.
1070 w.Header().Set("Etag", makeEtag(r.URL.Path, hash))
1071 _, err = w.Write(buf.Bytes())
1072 if err != nil {
1073 return APIError{HTTPStatus: http.StatusInternalServerError, Err: err}
1074 }
1075
1076 return nil
1077
1078 case http.MethodPost,
1079 http.MethodPut,
1080 http.MethodPatch,
1081 http.MethodDelete:
1082
1083 // DELETE does not use a body, but the others do
1084 var body []byte
1085 if r.Method != http.MethodDelete {
1086 if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
1087 return APIError{
1088 HTTPStatus: http.StatusBadRequest,
1089 Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
1090 }
1091 }
1092
1093 buf := bufPool.Get().(*bytes.Buffer)
1094 buf.Reset()
1095 defer bufPool.Put(buf)
1096
1097 const maxConfigSize = 100 * 1024 * 1024 // 100 MB
1098 r.Body = http.MaxBytesReader(w, r.Body, maxConfigSize)
1099
1100 _, err := io.Copy(buf, r.Body)
1101 if err != nil {
1102 return APIError{
1103 HTTPStatus: http.StatusBadRequest,
1104 Err: fmt.Errorf("reading request body: %v", err),
1105 }
1106 }
1107 body = buf.Bytes()
1108 }
1109
1110 forceReload := httpguts.HeaderValuesContainsToken(r.Header["Cache-Control"], "must-revalidate")
1111
1112 err := changeConfig(r.Method, r.URL.Path, body, r.Header.Get("If-Match"), forceReload)
1113 if err != nil && !errors.Is(err, errSameConfig) {
1114 return err
1115 }
1116
1117 // If this request changed the config, clear the last
1118 // config info we have stored, if it is different from
1119 // the original source.
1120 ClearLastConfigIfDifferent(
1121 r.Header.Get("Caddy-Config-Source-File"),
1122 r.Header.Get("Caddy-Config-Source-Adapter"))
1123
1124 default:
1125 return APIError{
1126 HTTPStatus: http.StatusMethodNotAllowed,
1127 Err: fmt.Errorf("method %s not allowed", r.Method),
1128 }
1129 }
1130
1131 return nil
1132 }
1133
1134 func handleConfigID(w http.ResponseWriter, r *http.Request) error {
1135 idPath := r.URL.Path
1136
1137 parts := strings.Split(idPath, "/")
1138 if len(parts) < 3 || parts[2] == "" {
1139 return APIError{
1140 HTTPStatus: http.StatusBadRequest,
1141 Err: fmt.Errorf("request path is missing object ID"),
1142 }
1143 }
1144 if parts[0] != "" || parts[1] != "id" {
1145 return APIError{
1146 HTTPStatus: http.StatusBadRequest,
1147 Err: fmt.Errorf("malformed object path"),
1148 }
1149 }
1150 id := parts[2]
1151
1152 // map the ID to the expanded path
1153 rawCfgMu.RLock()
1154 expanded, ok := rawCfgIndex[id]
1155 rawCfgMu.RUnlock()
1156 if !ok {
1157 return APIError{
1158 HTTPStatus: http.StatusNotFound,
1159 Err: fmt.Errorf("unknown object ID '%s'", id),
1160 }
1161 }
1162
1163 // piece the full URL path back together
1164 parts = append([]string{expanded}, parts[3:]...)
1165 r.URL.Path = path.Join(parts...)
1166
1167 return errInternalRedir
1168 }
1169
1170 func handleStop(w http.ResponseWriter, r *http.Request) error {
1171 if r.Method != http.MethodPost {
1172 return APIError{
1173 HTTPStatus: http.StatusMethodNotAllowed,
1174 Err: fmt.Errorf("method not allowed"),
1175 }
1176 }
1177
1178 exitProcess(context.Background(), Log().Named("admin.api"))
1179 return nil
1180 }
1181
1182 func parseCanonicalArrayIndex(idx string) (int, error) {
1183 if idx == "" {
1184 return 0, fmt.Errorf("empty index")
1185 }
1186 i, err := strconv.Atoi(idx)
1187 if err != nil {
1188 return 0, err
1189 }
1190 if strconv.Itoa(i) != idx {
1191 return 0, fmt.Errorf("non-canonical array index")
1192 }
1193 return i, nil
1194 }
1195
1196 // unsyncedConfigAccess traverses into the current config and performs
1197 // the operation at path according to method, using body and out as
1198 // needed. This is a low-level, unsynchronized function; most callers
1199 // will want to use changeConfig or readConfig instead. This requires a
1200 // read or write lock on currentCtxMu, depending on method (GET needs
1201 // only a read lock; all others need a write lock).
1202 func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
1203 var err error
1204 var val any
1205
1206 // if there is a request body, decode it into the
1207 // variable that will be set in the config according
1208 // to method and path
1209 if len(body) > 0 {
1210 err = json.Unmarshal(body, &val)
1211 if err != nil {
1212 if jsonErr, ok := err.(*json.SyntaxError); ok {
1213 return fmt.Errorf("decoding request body: %w, at offset %d", jsonErr, jsonErr.Offset)
1214 }
1215 return fmt.Errorf("decoding request body: %w", err)
1216 }
1217 }
1218
1219 enc := json.NewEncoder(out)
1220
1221 cleanPath := strings.Trim(path, "/")
1222 if cleanPath == "" {
1223 return fmt.Errorf("no traversable path")
1224 }
1225
1226 parts := strings.Split(cleanPath, "/")
1227 if len(parts) == 0 {
1228 return fmt.Errorf("path missing")
1229 }
1230
1231 // A path that ends with "..." implies:
1232 // 1) the part before it is an array
1233 // 2) the payload is an array
1234 // and means that the user wants to expand the elements
1235 // in the payload array and append each one into the
1236 // destination array, like so:
1237 // array = append(array, elems...)
1238 // This special case is handled below.
1239 ellipses := parts[len(parts)-1] == "..."
1240 if ellipses {
1241 parts = parts[:len(parts)-1]
1242 }
1243
1244 var ptr any = rawCfg
1245
1246 traverseLoop:
1247 for i, part := range parts {
1248 switch v := ptr.(type) {
1249 case map[string]any:
1250 // if the next part enters a slice, and the slice is our destination,
1251 // handle it specially (because appending to the slice copies the slice
1252 // header, which does not replace the original one like we want)
1253 if arr, ok := v[part].([]any); ok && i == len(parts)-2 {
1254 var idx int
1255 if method != http.MethodPost {
1256 idxStr := parts[len(parts)-1]
1257 idx, err = parseCanonicalArrayIndex(idxStr)
1258 if err != nil {
1259 return fmt.Errorf("[%s] invalid array index '%s': %v",
1260 path, idxStr, err)
1261 }
1262
1263 if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
1264 return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
1265 }
1266 }
1267
1268 switch method {
1269 case http.MethodGet:
1270 err = enc.Encode(arr[idx])
1271 if err != nil {
1272 return fmt.Errorf("encoding config: %v", err)
1273 }
1274 case http.MethodPost:
1275 if ellipses {
1276 valArray, ok := val.([]any)
1277 if !ok {
1278 return fmt.Errorf("final element is not an array")
1279 }
1280 v[part] = append(arr, valArray...)
1281 } else {
1282 v[part] = append(arr, val)
1283 }
1284 case http.MethodPut:
1285 // avoid creation of new slice and a second copy (see
1286 // https://github.com/golang/go/wiki/SliceTricks#insert)
1287 arr = append(arr, nil)
1288 copy(arr[idx+1:], arr[idx:])
1289 arr[idx] = val
1290 v[part] = arr
1291 case http.MethodPatch:
1292 arr[idx] = val
1293 case http.MethodDelete:
1294 v[part] = append(arr[:idx], arr[idx+1:]...)
1295 default:
1296 return fmt.Errorf("unrecognized method %s", method)
1297 }
1298 break traverseLoop
1299 }
1300
1301 if i == len(parts)-1 {
1302 switch method {
1303 case http.MethodGet:
1304 err = enc.Encode(v[part])
1305 if err != nil {
1306 return fmt.Errorf("encoding config: %v", err)
1307 }
1308 case http.MethodPost:
1309 // if the part is an existing list, POST appends to
1310 // it, otherwise it just sets or creates the value
1311 if arr, ok := v[part].([]any); ok {
1312 if ellipses {
1313 valArray, ok := val.([]any)
1314 if !ok {
1315 return fmt.Errorf("final element is not an array")
1316 }
1317 v[part] = append(arr, valArray...)
1318 } else {
1319 v[part] = append(arr, val)
1320 }
1321 } else {
1322 v[part] = val
1323 }
1324 case http.MethodPut:
1325 if _, ok := v[part]; ok {
1326 return APIError{
1327 HTTPStatus: http.StatusConflict,
1328 Err: fmt.Errorf("[%s] key already exists: %s", path, part),
1329 }
1330 }
1331 v[part] = val
1332 case http.MethodPatch:
1333 if _, ok := v[part]; !ok {
1334 return APIError{
1335 HTTPStatus: http.StatusNotFound,
1336 Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
1337 }
1338 }
1339 v[part] = val
1340 case http.MethodDelete:
1341 if _, ok := v[part]; !ok {
1342 return APIError{
1343 HTTPStatus: http.StatusNotFound,
1344 Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
1345 }
1346 }
1347 delete(v, part)
1348 default:
1349 return fmt.Errorf("unrecognized method %s", method)
1350 }
1351 } else {
1352 // if we are "PUTting" a new resource, the key(s) in its path
1353 // might not exist yet; that's OK but we need to make them as
1354 // we go, while we still have a pointer from the level above
1355 if v[part] == nil && method == http.MethodPut {
1356 v[part] = make(map[string]any)
1357 }
1358 ptr = v[part]
1359 }
1360
1361 case []any:
1362 partInt, err := parseCanonicalArrayIndex(part)
1363 if err != nil {
1364 return fmt.Errorf("[/%s] invalid array index '%s': %v",
1365 strings.Join(parts[:i+1], "/"), part, err)
1366 }
1367 if partInt < 0 || partInt >= len(v) {
1368 return fmt.Errorf("[/%s] array index out of bounds: %s",
1369 strings.Join(parts[:i+1], "/"), part)
1370 }
1371 ptr = v[partInt]
1372
1373 default:
1374 return fmt.Errorf("invalid traversal path at: %s", strings.Join(parts[:i+1], "/"))
1375 }
1376 }
1377
1378 return nil
1379 }
1380
1381 // RemoveMetaFields removes meta fields like "@id" from a JSON message
1382 // by using a simple regular expression. (An alternate way to do this
1383 // would be to delete them from the raw, map[string]any
1384 // representation as they are indexed, then iterate the index we made
1385 // and add them back after encoding as JSON, but this is simpler.)
1386 func RemoveMetaFields(rawJSON []byte) []byte {
1387 return idRegexp.ReplaceAllFunc(rawJSON, func(in []byte) []byte {
1388 // matches with a comma on both sides (when "@id" property is
1389 // not the first or last in the object) need to keep exactly
1390 // one comma for correct JSON syntax
1391 comma := []byte{','}
1392 if bytes.HasPrefix(in, comma) && bytes.HasSuffix(in, comma) {
1393 return comma
1394 }
1395 return []byte{}
1396 })
1397 }
1398
1399 // AdminHandler is like http.Handler except ServeHTTP may return an error.
1400 //
1401 // If any handler encounters an error, it should be returned for proper
1402 // handling.
1403 type AdminHandler interface {
1404 ServeHTTP(http.ResponseWriter, *http.Request) error
1405 }
1406
1407 // AdminHandlerFunc is a convenience type like http.HandlerFunc.
1408 type AdminHandlerFunc func(http.ResponseWriter, *http.Request) error
1409
1410 // ServeHTTP implements the Handler interface.
1411 func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
1412 return f(w, r)
1413 }
1414
1415 // APIError is a structured error that every API
1416 // handler should return for consistency in logging
1417 // and client responses. If Message is unset, then
1418 // Err.Error() will be serialized in its place.
1419 type APIError struct {
1420 HTTPStatus int `json:"-"`
1421 Err error `json:"-"`
1422 Message string `json:"error"`
1423 Warnings []warning.Warning `json:"warnings,omitempty"`
1424 }
1425
1426 func (e APIError) Error() string {
1427 if e.Err != nil {
1428 return e.Err.Error()
1429 }
1430 return e.Message
1431 }
1432
1433 // parseAdminListenAddr extracts a singular listen address from either addr
1434 // or defaultAddr, returning the network and the address of the listener.
1435 func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
1436 input, err := NewReplacer().ReplaceOrErr(addr, true, true)
1437 if err != nil {
1438 return NetworkAddress{}, fmt.Errorf("replacing listen address: %v", err)
1439 }
1440 if input == "" {
1441 input = defaultAddr
1442 }
1443 listenAddr, err := ParseNetworkAddress(input)
1444 if err != nil {
1445 return NetworkAddress{}, fmt.Errorf("parsing listener address: %v", err)
1446 }
1447 if listenAddr.PortRangeSize() != 1 {
1448 return NetworkAddress{}, fmt.Errorf("must be exactly one listener address; cannot listen on: %s", listenAddr)
1449 }
1450 return listenAddr, nil
1451 }
1452
1453 // decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
1454 func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
1455 derBytes, err := base64.StdEncoding.DecodeString(certStr)
1456 if err != nil {
1457 return nil, err
1458 }
1459 return x509.ParseCertificate(derBytes)
1460 }
1461
1462 type loggableURLArray []*url.URL
1463
1464 func (ua loggableURLArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
1465 if ua == nil {
1466 return nil
1467 }
1468 for _, u := range ua {
1469 enc.AppendString(u.String())
1470 }
1471 return nil
1472 }
1473
1474 var (
1475 // DefaultAdminListen is the address for the local admin
1476 // listener, if none is specified at startup.
1477 DefaultAdminListen = "localhost:2019"
1478
1479 // DefaultRemoteAdminListen is the address for the remote
1480 // (TLS-authenticated) admin listener, if enabled and not
1481 // specified otherwise.
1482 DefaultRemoteAdminListen = ":2021"
1483 )
1484
1485 // PIDFile writes a pidfile to the file at filename. It
1486 // will get deleted before the process gracefully exits.
1487 func PIDFile(filename string) error {
1488 pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
1489 err := os.WriteFile(filename, pid, 0o600)
1490 if err != nil {
1491 return err
1492 }
1493 pidfile = filename
1494 return nil
1495 }
1496
1497 // idRegexp is used to match ID fields and their associated values
1498 // in the config. It also matches adjacent commas so that syntax
1499 // can be preserved no matter where in the object the field appears.
1500 // It supports string and most numeric values.
1501 var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `"\s*:\s*(-?[0-9]+(\.[0-9]+)?|(?U)".*")\s*,?`)
1502
1503 // pidfile is the name of the pidfile, if any.
1504 var pidfile string
1505
1506 // errInternalRedir indicates an internal redirect
1507 // and is useful when admin API handlers rewrite
1508 // the request; in that case, authentication and
1509 // authorization needs to happen again for the
1510 // rewritten request.
1511 var errInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
1512
1513 const (
1514 rawConfigKey = "config"
1515 idKey = "@id"
1516 )
1517
1518 var bufPool = sync.Pool{
1519 New: func() any {
1520 return new(bytes.Buffer)
1521 },
1522 }
1523
1524 // keep a reference to admin endpoint singletons while they're active
1525 var (
1526 serverMu sync.Mutex
1527 localAdminServer, remoteAdminServer *http.Server
1528 identityCertCache *certmagic.Cache
1529 )