17
import (
18
"bytes"
19
"context"
20
+ "crypto"
21
+ "crypto/tls"
22
+ "crypto/x509"
23
+ "encoding/base64"
24
"encoding/json"
25
"errors"
26
"expvar"
39
"sync"
40
"time"
41
42
+ "github.com/caddyserver/certmagic"
43
"github.com/prometheus/client_golang/prometheus"
44
"go.uber.org/zap"
45
)
46
42
-// TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833
43
-
47
// AdminConfig configures Caddy's API endpoint, which is used
48
// to manage Caddy while it is running.
49
type AdminConfig struct {
61
// If true, CORS headers will be emitted, and requests to the
62
// API will be rejected if their `Host` and `Origin` headers
63
// do not match the expected value(s). Use `origins` to
61
- // customize which origins/hosts are allowed.If `origins` is
64
+ // customize which origins/hosts are allowed. If `origins` is
65
// not set, the listen address is the only value allowed by
63
- // default.
66
+ // default. Enforced only on local (plaintext) endpoint.
67
EnforceOrigin bool `json:"enforce_origin,omitempty"`
68
69
// The list of allowed origins/hosts for API requests. Only needed
70
// if accessing the admin endpoint from a host different from the
71
// socket's network interface or if `enforce_origin` is true. If not
72
// set, the listener address will be the default value. If set but
70
- // empty, no origins will be allowed.
73
+ // empty, no origins will be allowed. Enforced only on local
74
+ // (plaintext) endpoint.
75
Origins []string `json:"origins,omitempty"`
76
73
- // Options related to configuration management.
77
+ // Options pertaining to configuration management.
78
Config *ConfigSettings `json:"config,omitempty"`
79
+
80
+ // Options that establish this server's identity. Identity refers to
81
+ // credentials which can be used to uniquely identify and authenticate
82
+ // this server instance. This is required if remote administration is
83
+ // enabled (but does not require remote administration to be enabled).
84
+ // Default: no identity management.
85
+ Identity *IdentityConfig `json:"identity,omitempty"`
86
+
87
+ // Options pertaining to remote administration. By default, remote
88
+ // administration is disabled. If enabled, identity management must
89
+ // also be configured, as that is how the endpoint is secured.
90
+ // See the neighboring "identity" object.
91
+ //
92
+ // EXPERIMENTAL: This feature is subject to change.
93
+ Remote *RemoteAdmin `json:"remote,omitempty"`
94
}
95
77
-// ConfigSettings configures the, uh, configuration... and
78
-// management thereof.
96
+// ConfigSettings configures the management of configuration.
97
type ConfigSettings struct {
98
// Whether to keep a copy of the active config on disk. Default is true.
99
+ // Note that "pulled" dynamic configs (using the neighboring "load" module)
100
+ // are not persisted; only configs that are pushed to Caddy get persisted.
101
Persist *bool `json:"persist,omitempty"`
102
+
103
+ // Loads a configuration to use. This is helpful if your configs are
104
+ // managed elsewhere, and you want Caddy to pull its config dynamically
105
+ // when it starts. The pulled config completely replaces the current
106
+ // one, just like any other config load. It is an error if a pulled
107
+ // config is configured to pull another config.
108
+ //
109
+ // EXPERIMENTAL: Subject to change.
110
+ LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
111
}
112
84
-// listenAddr extracts a singular listen address from ac.Listen,
85
-// returning the network and the address of the listener.
86
-func (admin AdminConfig) listenAddr() (NetworkAddress, error) {
87
- input := admin.Listen
88
- if input == "" {
89
- input = DefaultAdminListen
90
- }
91
- listenAddr, err := ParseNetworkAddress(input)
92
- if err != nil {
93
- return NetworkAddress{}, fmt.Errorf("parsing admin listener address: %v", err)
94
- }
95
- if listenAddr.PortRangeSize() != 1 {
96
- return NetworkAddress{}, fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
97
- }
98
- return listenAddr, nil
113
+// IdentityConfig configures management of this server's identity. An identity
114
+// consists of credentials that uniquely verify this instance; for example,
115
+// TLS certificates (public + private key pairs).
116
+type IdentityConfig struct {
117
+ // List of names or IP addresses which refer to this server.
118
+ // Certificates will be obtained for these identifiers so
119
+ // secure TLS connections can be made using them.
120
+ Identifiers []string `json:"identifiers,omitempty"`
121
+
122
+ // Issuers that can provide this admin endpoint its identity
123
+ // certificate(s). Default: ACME issuers configured for
124
+ // ZeroSSL and Let's Encrypt. Be sure to change this if you
125
+ // require credentials for private identifiers.
126
+ IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
127
+
128
+ issuers []certmagic.Issuer
129
+}
130
+
131
+// RemoteAdmin enables and configures remote administration. If enabled,
132
+// a secure listener enforcing mutual TLS authentication will be started
133
+// on a different port from the standard plaintext admin server.
134
+//
135
+// This endpoint is secured using identity management, which must be
136
+// configured separately (because identity management does not depend
137
+// on remote administration). See the admin/identity config struct.
138
+//
139
+// EXPERIMENTAL: Subject to change.
140
+type RemoteAdmin struct {
141
+ // The address on which to start the secure listener.
142
+ // Default: :2021
143
+ Listen string `json:"listen,omitempty"`
144
+
145
+ // List of access controls for this secure admin endpoint.
146
+ // This configures TLS mutual authentication (i.e. authorized
147
+ // client certificates), but also application-layer permissions
148
+ // like which paths and methods each identity is authorized for.
149
+ AccessControl []*AdminAccess `json:"access_control,omitempty"`
150
+}
151
+
152
+// AdminAccess specifies what permissions an identity or group
153
+// of identities are granted.
154
+type AdminAccess struct {
155
+ // Base64-encoded DER certificates containing public keys to accept.
156
+ // (The contents of PEM certificate blocks are base64-encoded DER.)
157
+ // Any of these public keys can appear in any part of a verified chain.
158
+ PublicKeys []string `json:"public_keys,omitempty"`
159
+
160
+ // Limits what the associated identities are allowed to do.
161
+ // If unspecified, all permissions are granted.
162
+ Permissions []AdminPermissions `json:"permissions,omitempty"`
163
+
164
+ publicKeys []crypto.PublicKey
165
+}
166
+
167
+// AdminPermissions specifies what kinds of requests are allowed
168
+// to be made to the admin endpoint.
169
+type AdminPermissions struct {
170
+ // The API paths allowed. Paths are simple prefix matches.
171
+ // Any subpath of the specified paths will be allowed.
172
+ Paths []string `json:"paths,omitempty"`
173
+
174
+ // The HTTP methods allowed for the given paths.
175
+ Methods []string `json:"methods,omitempty"`
176
}
177
178
// newAdminHandler reads admin's config and returns an http.Handler suitable
179
// for use in an admin endpoint server, which will be listening on listenAddr.
103
-func (admin AdminConfig) newAdminHandler(addr NetworkAddress) adminHandler {
104
- muxWrap := adminHandler{
105
- enforceOrigin: admin.EnforceOrigin,
106
- enforceHost: !addr.isWildcardInterface(),
107
- allowedOrigins: admin.allowedOrigins(addr),
108
- mux: http.NewServeMux(),
180
+func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
181
+ muxWrap := adminHandler{mux: http.NewServeMux()}
182
+
183
+ // secure the local or remote endpoint respectively
184
+ if remote {
185
+ muxWrap.remoteControl = admin.Remote
186
+ } else {
187
+ muxWrap.enforceHost = !addr.isWildcardInterface()
188
+ muxWrap.allowedOrigins = admin.allowedOrigins(addr)
189
}
190
191
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
277
return allowed
278
}
279
200
-// replaceAdmin replaces the running admin server according
201
-// to the relevant configuration in cfg. If no configuration
202
-// for the admin endpoint exists in cfg, a default one is
203
-// used, so that there is always an admin server (unless it
204
-// is explicitly configured to be disabled).
205
-func replaceAdmin(cfg *Config) error {
280
+// replaceLocalAdminServer replaces the running local admin server
281
+// according to the relevant configuration in cfg. If no configuration
282
+// for the admin endpoint exists in cfg, a default one is used, so
283
+// that there is always an admin server (unless it is explicitly
284
+// configured to be disabled).
285
+func replaceLocalAdminServer(cfg *Config) error {
286
// always be sure to close down the old admin endpoint
287
// as gracefully as possible, even if the new one is
288
// disabled -- careful to use reference to the current
289
// (old) admin endpoint since it will be different
290
// when the function returns
211
- oldAdminServer := adminServer
291
+ oldAdminServer := localAdminServer
292
defer func() {
293
// do the shutdown asynchronously so that any
294
// current API request gets a response; this
316
}
317
318
// extract a singular listener address
239
- addr, err := adminConfig.listenAddr()
319
+ addr, err := parseAdminListenAddr(adminConfig.Listen, DefaultAdminListen)
320
if err != nil {
321
return err
322
}
323
244
- handler := adminConfig.newAdminHandler(addr)
324
+ handler := adminConfig.newAdminHandler(addr, false)
325
326
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
327
if err != nil {
328
return err
329
}
330
251
- adminServer = &http.Server{
331
+ localAdminServer = &http.Server{
332
+ Addr: addr.String(), // for logging purposes only
333
Handler: handler,
334
ReadTimeout: 10 * time.Second,
335
ReadHeaderTimeout: 5 * time.Second,
339
340
adminLogger := Log().Named("admin")
341
go func() {
261
- if err := adminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
342
+ if err := localAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
343
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
344
}
345
}()
357
return nil
358
}
359
360
+// manageIdentity sets up automated identity management for this server.
361
+func manageIdentity(ctx Context, cfg *Config) error {
362
+ if cfg == nil || cfg.Admin == nil || cfg.Admin.Identity == nil {
363
+ return nil
364
+ }
365
+
366
+ oldIdentityCertCache := identityCertCache
367
+ if oldIdentityCertCache != nil {
368
+ defer oldIdentityCertCache.Stop()
369
+ }
370
+
371
+ // set default issuers; this is pretty hacky because we can't
372
+ // import the caddytls package -- but it works
373
+ if cfg.Admin.Identity.IssuersRaw == nil {
374
+ cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
375
+ json.RawMessage(`{"module": "zerossl"}`),
376
+ json.RawMessage(`{"module": "acme"}`),
377
+ }
378
+ }
379
+
380
+ // load and provision issuer modules
381
+ if cfg.Admin.Identity.IssuersRaw != nil {
382
+ val, err := ctx.LoadModule(cfg.Admin.Identity, "IssuersRaw")
383
+ if err != nil {
384
+ return fmt.Errorf("loading identity issuer modules: %s", err)
385
+ }
386
+ for _, issVal := range val.([]interface{}) {
387
+ cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
388
+ }
389
+ }
390
+
391
+ logger := Log().Named("admin.identity")
392
+ cmCfg := cfg.Admin.Identity.certmagicConfig(logger)
393
+
394
+ // issuers have circular dependencies with the configs because,
395
+ // as explained in the caddytls package, they need access to the
396
+ // correct storage and cache to solve ACME challenges
397
+ for _, issuer := range cfg.Admin.Identity.issuers {
398
+ // avoid import cycle with caddytls package, so manually duplicate the interface here, yuck
399
+ if annoying, ok := issuer.(interface{ SetConfig(cfg *certmagic.Config) }); ok {
400
+ annoying.SetConfig(cmCfg)
401
+ }
402
+ }
403
+
404
+ // obtain and renew server identity certificate(s)
405
+ return cmCfg.ManageAsync(ctx, cfg.Admin.Identity.Identifiers)
406
+}
407
+
408
+// replaceRemoteAdminServer replaces the running remote admin server
409
+// according to the relevant configuration in cfg. It stops any previous
410
+// remote admin server and only starts a new one if configured.
411
+func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
412
+ if cfg == nil {
413
+ return nil
414
+ }
415
+
416
+ remoteLogger := Log().Named("admin.remote")
417
+
418
+ oldAdminServer := remoteAdminServer
419
+ defer func() {
420
+ if oldAdminServer != nil {
421
+ go func(oldAdminServer *http.Server) {
422
+ err := stopAdminServer(oldAdminServer)
423
+ if err != nil {
424
+ Log().Named("admin").Error("stopping current secure admin endpoint", zap.Error(err))
425
+ }
426
+ }(oldAdminServer)
427
+ }
428
+ }()
429
+
430
+ if cfg.Admin == nil || cfg.Admin.Remote == nil {
431
+ return nil
432
+ }
433
+
434
+ addr, err := parseAdminListenAddr(cfg.Admin.Remote.Listen, DefaultRemoteAdminListen)
435
+ if err != nil {
436
+ return err
437
+ }
438
+
439
+ // make the HTTP handler but disable Host/Origin enforcement
440
+ // because we are using TLS authentication instead
441
+ handler := cfg.Admin.newAdminHandler(addr, true)
442
+
443
+ // create client certificate pool for TLS mutual auth, and extract public keys
444
+ // so that we can enforce access controls at the application layer
445
+ clientCertPool := x509.NewCertPool()
446
+ for i, accessControl := range cfg.Admin.Remote.AccessControl {
447
+ for j, certBase64 := range accessControl.PublicKeys {
448
+ cert, err := decodeBase64DERCert(certBase64)
449
+ if err != nil {
450
+ return fmt.Errorf("access control %d public key %d: parsing base64 certificate DER: %v", i, j, err)
451
+ }
452
+ accessControl.publicKeys = append(accessControl.publicKeys, cert.PublicKey)
453
+ clientCertPool.AddCert(cert)
454
+ }
455
+ }
456
+
457
+ // create TLS config that will enforce mutual authentication
458
+ cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger)
459
+ tlsConfig := cmCfg.TLSConfig()
460
+ tlsConfig.NextProtos = nil // this server does not solve ACME challenges
461
+ tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
462
+ tlsConfig.ClientCAs = clientCertPool
463
+
464
+ // convert logger to stdlib so it can be used by HTTP server
465
+ serverLogger, err := zap.NewStdLogAt(remoteLogger, zap.DebugLevel)
466
+ if err != nil {
467
+ return err
468
+ }
469
+
470
+ // create secure HTTP server
471
+ remoteAdminServer = &http.Server{
472
+ Addr: addr.String(), // for logging purposes only
473
+ Handler: handler,
474
+ TLSConfig: tlsConfig,
475
+ ReadTimeout: 10 * time.Second,
476
+ ReadHeaderTimeout: 5 * time.Second,
477
+ IdleTimeout: 60 * time.Second,
478
+ MaxHeaderBytes: 1024 * 64,
479
+ ErrorLog: serverLogger,
480
+ }
481
+
482
+ // start listener
483
+ ln, err := Listen(addr.Network, addr.JoinHostPort(0))
484
+ if err != nil {
485
+ return err
486
+ }
487
+ ln = tls.NewListener(ln, tlsConfig)
488
+
489
+ go func() {
490
+ if err := remoteAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
491
+ remoteLogger.Error("admin remote server shutdown for unknown reason", zap.Error(err))
492
+ }
493
+ }()
494
+
495
+ remoteLogger.Info("secure admin remote control endpoint started",
496
+ zap.String("address", addr.String()))
497
+
498
+ return nil
499
+}
500
+
501
+func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger) *certmagic.Config {
502
+ if ident == nil {
503
+ // user might not have configured identity; that's OK, we can still make a
504
+ // certmagic config, although it'll be mostly useless for remote management
505
+ ident = new(IdentityConfig)
506
+ }
507
+ cmCfg := &certmagic.Config{
508
+ Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity)
509
+ Logger: logger,
510
+ Issuers: ident.issuers,
511
+ }
512
+ if identityCertCache == nil {
513
+ identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
514
+ GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
515
+ return cmCfg, nil
516
+ },
517
+ })
518
+ }
519
+ return certmagic.New(identityCertCache, *cmCfg)
520
+}
521
+
522
+// IdentityCredentials returns this instance's configured, managed identity credentials
523
+// that can be used in TLS client authentication.
524
+func (ctx Context) IdentityCredentials(logger *zap.Logger) ([]tls.Certificate, error) {
525
+ if ctx.cfg == nil || ctx.cfg.Admin == nil || ctx.cfg.Admin.Identity == nil {
526
+ return nil, fmt.Errorf("no server identity configured")
527
+ }
528
+ ident := ctx.cfg.Admin.Identity
529
+ if len(ident.Identifiers) == 0 {
530
+ return nil, fmt.Errorf("no identifiers configured")
531
+ }
532
+ if logger == nil {
533
+ logger = Log()
534
+ }
535
+ magic := ident.certmagicConfig(logger)
536
+ return magic.ClientCredentials(ctx, ident.Identifiers)
537
+}
538
+
539
+// enforceAccessControls enforces application-layer access controls for r based on remote.
540
+// It expects that the TLS server has already established at least one verified chain of
541
+// trust, and then looks for a matching, authorized public key that is allowed to access
542
+// the defined path(s) using the defined method(s).
543
+func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
544
+ for _, chain := range r.TLS.VerifiedChains {
545
+ for _, peerCert := range chain {
546
+ for _, adminAccess := range remote.AccessControl {
547
+ for _, allowedKey := range adminAccess.publicKeys {
548
+ // see if we found a matching public key; the TLS server already verified the chain
549
+ // so we know the client possesses the associated private key; this handy interface
550
+ // doesn't appear to be defined anywhere in the std lib, but was implemented here:
551
+ // https://github.com/golang/go/commit/b5f2c0f50297fa5cd14af668ddd7fd923626cf8c
552
+ comparer, ok := peerCert.PublicKey.(interface{ Equal(crypto.PublicKey) bool })
553
+ if !ok || !comparer.Equal(allowedKey) {
554
+ continue
555
+ }
556
+
557
+ // key recognized; make sure its HTTP request is permitted
558
+ for _, accessPerm := range adminAccess.Permissions {
559
+ // verify method
560
+ methodFound := accessPerm.Methods == nil
561
+ for _, method := range accessPerm.Methods {
562
+ if method == r.Method {
563
+ methodFound = true
564
+ break
565
+ }
566
+ }
567
+ if !methodFound {
568
+ return APIError{
569
+ HTTPStatus: http.StatusForbidden,
570
+ Message: "not authorized to use this method",
571
+ }
572
+ }
573
+
574
+ // verify path
575
+ pathFound := accessPerm.Paths == nil
576
+ for _, allowedPath := range accessPerm.Paths {
577
+ if strings.HasPrefix(r.URL.Path, allowedPath) {
578
+ pathFound = true
579
+ break
580
+ }
581
+ }
582
+ if !pathFound {
583
+ return APIError{
584
+ HTTPStatus: http.StatusForbidden,
585
+ Message: "not authorized to access this path",
586
+ }
587
+ }
588
+ }
589
+
590
+ // public key authorized, method and path allowed
591
+ return nil
592
+ }
593
+ }
594
+ }
595
+ }
596
+
597
+ // in theory, this should never happen; with an unverified chain, the TLS server
598
+ // should not accept the connection in the first place, and the acceptable cert
599
+ // pool is configured using the same list of public keys we verify against
600
+ return APIError{
601
+ HTTPStatus: http.StatusUnauthorized,
602
+ Message: "client identity not authorized",
603
+ }
604
+}
605
+
606
func stopAdminServer(srv *http.Server) error {
607
if srv == nil {
608
return fmt.Errorf("no admin server")
613
if err != nil {
614
return fmt.Errorf("shutting down admin server: %v", err)
615
}
289
- Log().Named("admin").Info("stopped previous server")
616
+ Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
617
return nil
618
}
619
629
}
630
631
type adminHandler struct {
632
+ mux *http.ServeMux
633
+
634
+ // security for local/plaintext) endpoint, on by default
635
enforceOrigin bool
636
enforceHost bool
637
allowedOrigins []string
308
- mux *http.ServeMux
638
+
639
+ // security for remote/encrypted endpoint
640
+ remoteControl *RemoteAdmin
641
}
642
643
// ServeHTTP is the external entry point for API requests.
650
zap.String("remote_addr", r.RemoteAddr),
651
zap.Reflect("headers", r.Header),
652
)
653
+ if r.TLS != nil {
654
+ log = log.With(
655
+ zap.Bool("secure", true),
656
+ zap.Int("verified_chains", len(r.TLS.VerifiedChains)),
657
+ )
658
+ }
659
if r.RequestURI == "/metrics" {
660
log.Debug("received request")
661
} else {
668
// be called more than once per request, for example if a request
669
// is rewritten (i.e. internal redirect).
670
func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
671
+ if h.remoteControl != nil {
672
+ // enforce access controls on secure endpoint
673
+ if err := h.remoteControl.enforceAccessControls(r); err != nil {
674
+ h.handleError(w, r, err)
675
+ return
676
+ }
677
+ }
678
+
679
if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
680
// I've never been able demonstrate a vulnerability myself, but apparently
681
// WebSocket connections originating from browsers aren't subject to CORS
709
w.Header().Set("Access-Control-Allow-Origin", origin)
710
}
711
366
- // TODO: authentication & authorization, if configured
367
-
712
h.mux.ServeHTTP(w, r)
713
}
714
716
if err == nil {
717
return
718
}
375
- if err == ErrInternalRedir {
376
- h.serveHTTP(w, r)
377
- return
378
- }
719
720
apiErr, ok := err.(APIError)
721
if !ok {
722
apiErr = APIError{
383
- Code: http.StatusInternalServerError,
384
- Err: err,
723
+ HTTPStatus: http.StatusInternalServerError,
724
+ Err: err,
725
}
726
}
387
- if apiErr.Code == 0 {
388
- apiErr.Code = http.StatusInternalServerError
727
+ if apiErr.HTTPStatus == 0 {
728
+ apiErr.HTTPStatus = http.StatusInternalServerError
729
}
730
if apiErr.Message == "" && apiErr.Err != nil {
731
apiErr.Message = apiErr.Err.Error()
733
734
Log().Named("admin.api").Error("request error",
735
zap.Error(err),
396
- zap.Int("status_code", apiErr.Code),
736
+ zap.Int("status_code", apiErr.HTTPStatus),
737
)
738
739
w.Header().Set("Content-Type", "application/json")
400
- w.WriteHeader(apiErr.Code)
740
+ w.WriteHeader(apiErr.HTTPStatus)
741
encErr := json.NewEncoder(w).Encode(apiErr)
742
if encErr != nil {
743
Log().Named("admin.api").Error("failed to encode error response", zap.Error(encErr))
758
}
759
if !allowed {
760
return APIError{
421
- Code: http.StatusForbidden,
422
- Err: fmt.Errorf("host not allowed: %s", r.Host),
761
+ HTTPStatus: http.StatusForbidden,
762
+ Err: fmt.Errorf("host not allowed: %s", r.Host),
763
}
764
}
765
return nil
773
origin := h.getOriginHost(r)
774
if origin == "" {
775
return origin, APIError{
436
- Code: http.StatusForbidden,
437
- Err: fmt.Errorf("missing required Origin header"),
776
+ HTTPStatus: http.StatusForbidden,
777
+ Err: fmt.Errorf("missing required Origin header"),
778
}
779
}
780
if !h.originAllowed(origin) {
781
return origin, APIError{
442
- Code: http.StatusForbidden,
443
- Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
782
+ HTTPStatus: http.StatusForbidden,
783
+ Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
784
}
785
}
786
return origin, nil
820
821
err := readConfig(r.URL.Path, w)
822
if err != nil {
483
- return APIError{Code: http.StatusBadRequest, Err: err}
823
+ return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
824
}
825
826
return nil
835
if r.Method != http.MethodDelete {
836
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
837
return APIError{
498
- Code: http.StatusBadRequest,
499
- Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
838
+ HTTPStatus: http.StatusBadRequest,
839
+ Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
840
}
841
}
842
847
_, err := io.Copy(buf, r.Body)
848
if err != nil {
849
return APIError{
510
- Code: http.StatusBadRequest,
511
- Err: fmt.Errorf("reading request body: %v", err),
850
+ HTTPStatus: http.StatusBadRequest,
851
+ Err: fmt.Errorf("reading request body: %v", err),
852
}
853
}
854
body = buf.Bytes()
863
864
default:
865
return APIError{
526
- Code: http.StatusMethodNotAllowed,
527
- Err: fmt.Errorf("method %s not allowed", r.Method),
866
+ HTTPStatus: http.StatusMethodNotAllowed,
867
+ Err: fmt.Errorf("method %s not allowed", r.Method),
868
}
869
}
870
895
parts = append([]string{expanded}, parts[3:]...)
896
r.URL.Path = path.Join(parts...)
897
558
- return ErrInternalRedir
559
-}
560
-
561
-func handleStop(w http.ResponseWriter, r *http.Request) error {
562
- err := handleUnload(w, r)
563
- if err != nil {
564
- Log().Named("admin.api").Error("unload error", zap.Error(err))
565
- }
566
- if adminServer != nil {
567
- // use goroutine so that we can finish responding to API request
568
- go func() {
569
- err := stopAdminServer(adminServer)
570
- var exitCode int
571
- if err != nil {
572
- exitCode = ExitCodeFailedQuit
573
- Log().Named("admin.api").Error("failed to stop admin server gracefully", zap.Error(err))
574
- }
575
- Log().Named("admin.api").Info("stopping now, bye!! 👋")
576
- os.Exit(exitCode)
577
- }()
578
- }
898
return nil
899
}
900
582
-// handleUnload stops the current configuration that is running.
583
-// Note that doing this can also be accomplished with DELETE /config/
584
-// but we leave this function because handleStop uses it.
585
-func handleUnload(w http.ResponseWriter, r *http.Request) error {
901
+func handleStop(w http.ResponseWriter, r *http.Request) error {
902
if r.Method != http.MethodPost {
903
return APIError{
588
- Code: http.StatusMethodNotAllowed,
589
- Err: fmt.Errorf("method not allowed"),
904
+ HTTPStatus: http.StatusMethodNotAllowed,
905
+ Err: fmt.Errorf("method not allowed"),
906
}
907
}
592
- Log().Named("admin.api").Info("unloading")
593
- if err := stopAndCleanup(); err != nil {
594
- Log().Named("admin.api").Error("error unloading", zap.Error(err))
595
- } else {
596
- Log().Named("admin.api").Info("unloading completed")
597
- }
908
+ exitProcess(Log().Named("admin.api"))
909
return nil
910
}
911
1117
// and client responses. If Message is unset, then
1118
// Err.Error() will be serialized in its place.
1119
type APIError struct {
809
- Code int `json:"-"`
810
- Err error `json:"-"`
811
- Message string `json:"error"`
1120
+ HTTPStatus int `json:"-"`
1121
+ Err error `json:"-"`
1122
+ Message string `json:"error"`
1123
}
1124
1125
func (e APIError) Error() string {
1129
return e.Message
1130
}
1131
1132
+// parseAdminListenAddr extracts a singular listen address from either addr
1133
+// or defaultAddr, returning the network and the address of the listener.
1134
+func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
1135
+ input := addr
1136
+ if input == "" {
1137
+ input = defaultAddr
1138
+ }
1139
+ listenAddr, err := ParseNetworkAddress(input)
1140
+ if err != nil {
1141
+ return NetworkAddress{}, fmt.Errorf("parsing listener address: %v", err)
1142
+ }
1143
+ if listenAddr.PortRangeSize() != 1 {
1144
+ return NetworkAddress{}, fmt.Errorf("must be exactly one listener address; cannot listen on: %s", listenAddr)
1145
+ }
1146
+ return listenAddr, nil
1147
+}
1148
+
1149
+// decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
1150
+func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
1151
+ derBytes, err := base64.StdEncoding.DecodeString(certStr)
1152
+ if err != nil {
1153
+ return nil, err
1154
+ }
1155
+ return x509.ParseCertificate(derBytes)
1156
+}
1157
+
1158
var (
822
- // DefaultAdminListen is the address for the admin
1159
+ // DefaultAdminListen is the address for the local admin
1160
// listener, if none is specified at startup.
1161
DefaultAdminListen = "localhost:2019"
1162
826
- // ErrInternalRedir indicates an internal redirect
827
- // and is useful when admin API handlers rewrite
828
- // the request; in that case, authentication and
829
- // authorization needs to happen again for the
830
- // rewritten request.
831
- ErrInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
1163
+ // DefaultRemoteAdminListen is the address for the remote
1164
+ // (TLS-authenticated) admin listener, if enabled and not
1165
+ // specified otherwise.
1166
+ DefaultRemoteAdminListen = ":2021"
1167
1168
// DefaultAdminConfig is the default configuration
834
- // for the administration endpoint.
1169
+ // for the local administration endpoint.
1170
DefaultAdminConfig = &AdminConfig{
1171
Listen: DefaultAdminListen,
1172
}
1204
},
1205
}
1206
872
-var adminServer *http.Server
1207
+// keep a reference to admin endpoint singletons while they're active
1208
+var (
1209
+ localAdminServer, remoteAdminServer *http.Server
1210
+ identityCertCache *certmagic.Cache
1211
+)