@ -17,6 +17,10 @@ package caddy
import (
import (
"bytes"
"bytes"
"context"
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/json"
"errors"
"errors"
"expvar"
"expvar"
@ -35,12 +39,11 @@ import (
"sync"
"sync"
"time"
"time"
"github.com/caddyserver/certmagic"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"
"go.uber.org/zap"
)
)
// TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833
// AdminConfig configures Caddy's API endpoint, which is used
// AdminConfig configures Caddy's API endpoint, which is used
// to manage Caddy while it is running.
// to manage Caddy while it is running.
type AdminConfig struct {
type AdminConfig struct {
@ -58,54 +61,131 @@ type AdminConfig struct {
// If true, CORS headers will be emitted, and requests to the
// If true, CORS headers will be emitted, and requests to the
// API will be rejected if their `Host` and `Origin` headers
// API will be rejected if their `Host` and `Origin` headers
// do not match the expected value(s). Use `origins` to
// do not match the expected value(s). Use `origins` to
// customize which origins/hosts are allowed.If `origins` is
// customize which origins/hosts are allowed. If `origins` is
// not set, the listen address is the only value allowed by
// not set, the listen address is the only value allowed by
// default.
// default. Enforced only on local (plaintext) endpoint.
EnforceOrigin bool ` json:"enforce_origin,omitempty" `
EnforceOrigin bool ` json:"enforce_origin,omitempty" `
// The list of allowed origins/hosts for API requests. Only needed
// The list of allowed origins/hosts for API requests. Only needed
// if accessing the admin endpoint from a host different from the
// if accessing the admin endpoint from a host different from the
// socket's network interface or if `enforce_origin` is true. If not
// socket's network interface or if `enforce_origin` is true. If not
// set, the listener address will be the default value. If set but
// set, the listener address will be the default value. If set but
// empty, no origins will be allowed.
// empty, no origins will be allowed. Enforced only on local
// (plaintext) endpoint.
Origins [ ] string ` json:"origins,omitempty" `
Origins [ ] string ` json:"origins,omitempty" `
// Options related to configuration management.
// Options pertaining to configuration management.
Config * ConfigSettings ` json:"config,omitempty" `
Config * ConfigSettings ` json:"config,omitempty" `
// Options that establish this server's identity. Identity refers to
// credentials which can be used to uniquely identify and authenticate
// this server instance. This is required if remote administration is
// enabled (but does not require remote administration to be enabled).
// Default: no identity management.
Identity * IdentityConfig ` json:"identity,omitempty" `
// Options pertaining to remote administration. By default, remote
// administration is disabled. If enabled, identity management must
// also be configured, as that is how the endpoint is secured.
// See the neighboring "identity" object.
//
// EXPERIMENTAL: This feature is subject to change.
Remote * RemoteAdmin ` json:"remote,omitempty" `
}
}
// ConfigSettings configures the, uh, configuration... and
// ConfigSettings configures the management of configuration.
// management thereof.
type ConfigSettings struct {
type ConfigSettings struct {
// Whether to keep a copy of the active config on disk. Default is true.
// Whether to keep a copy of the active config on disk. Default is true.
// Note that "pulled" dynamic configs (using the neighboring "load" module)
// are not persisted; only configs that are pushed to Caddy get persisted.
Persist * bool ` json:"persist,omitempty" `
Persist * bool ` json:"persist,omitempty" `
// Loads a configuration to use. This is helpful if your configs are
// managed elsewhere, and you want Caddy to pull its config dynamically
// when it starts. The pulled config completely replaces the current
// one, just like any other config load. It is an error if a pulled
// config is configured to pull another config.
//
// EXPERIMENTAL: Subject to change.
LoadRaw json . RawMessage ` json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module" `
}
}
// listenAddr extracts a singular listen address from ac.Listen,
// IdentityConfig configures management of this server's identity. An identity
// returning the network and the address of the listener.
// consists of credentials that uniquely verify this instance; for example,
func ( admin AdminConfig ) listenAddr ( ) ( NetworkAddress , error ) {
// TLS certificates (public + private key pairs).
input := admin . Listen
type IdentityConfig struct {
if input == "" {
// List of names or IP addresses which refer to this server.
input = DefaultAdminListen
// Certificates will be obtained for these identifiers so
}
// secure TLS connections can be made using them.
listenAddr , err := ParseNetworkAddress ( input )
Identifiers [ ] string ` json:"identifiers,omitempty" `
if err != nil {
return NetworkAddress { } , fmt . Errorf ( "parsing admin listener address: %v" , err )
// Issuers that can provide this admin endpoint its identity
}
// certificate(s). Default: ACME issuers configured for
if listenAddr . PortRangeSize ( ) != 1 {
// ZeroSSL and Let's Encrypt. Be sure to change this if you
return NetworkAddress { } , fmt . Errorf ( "admin endpoint must have exactly one address; cannot listen on %v" , listenAddr )
// require credentials for private identifiers.
}
IssuersRaw [ ] json . RawMessage ` json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module" `
return listenAddr , nil
issuers [ ] certmagic . Issuer
}
// RemoteAdmin enables and configures remote administration. If enabled,
// a secure listener enforcing mutual TLS authentication will be started
// on a different port from the standard plaintext admin server.
//
// This endpoint is secured using identity management, which must be
// configured separately (because identity management does not depend
// on remote administration). See the admin/identity config struct.
//
// EXPERIMENTAL: Subject to change.
type RemoteAdmin struct {
// The address on which to start the secure listener.
// Default: :2021
Listen string ` json:"listen,omitempty" `
// List of access controls for this secure admin endpoint.
// This configures TLS mutual authentication (i.e. authorized
// client certificates), but also application-layer permissions
// like which paths and methods each identity is authorized for.
AccessControl [ ] * AdminAccess ` json:"access_control,omitempty" `
}
// AdminAccess specifies what permissions an identity or group
// of identities are granted.
type AdminAccess struct {
// Base64-encoded DER certificates containing public keys to accept.
// (The contents of PEM certificate blocks are base64-encoded DER.)
// Any of these public keys can appear in any part of a verified chain.
PublicKeys [ ] string ` json:"public_keys,omitempty" `
// Limits what the associated identities are allowed to do.
// If unspecified, all permissions are granted.
Permissions [ ] AdminPermissions ` json:"permissions,omitempty" `
publicKeys [ ] crypto . PublicKey
}
// AdminPermissions specifies what kinds of requests are allowed
// to be made to the admin endpoint.
type AdminPermissions struct {
// The API paths allowed. Paths are simple prefix matches.
// Any subpath of the specified paths will be allowed.
Paths [ ] string ` json:"paths,omitempty" `
// The HTTP methods allowed for the given paths.
Methods [ ] string ` json:"methods,omitempty" `
}
}
// newAdminHandler reads admin's config and returns an http.Handler suitable
// newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr.
// for use in an admin endpoint server, which will be listening on listenAddr.
func ( admin AdminConfig ) newAdminHandler ( addr NetworkAddress ) adminHandler {
func ( admin AdminConfig ) newAdminHandler ( addr NetworkAddress , remote bool ) adminHandler {
muxWrap := adminHandler {
muxWrap := adminHandler { mux : http . NewServeMux ( ) }
enforceOrigin : admin . EnforceOrigin ,
enforceHost : ! addr . isWildcardInterface ( ) ,
// secure the local or remote endpoint respectively
allowedOrigins : admin . allowedOrigins ( addr ) ,
if remote {
mux : http . NewServeMux ( ) ,
muxWrap . remoteControl = admin . Remote
} else {
muxWrap . enforceHost = ! addr . isWildcardInterface ( )
muxWrap . allowedOrigins = admin . allowedOrigins ( addr )
}
}
addRouteWithMetrics := func ( pattern string , handlerLabel string , h http . Handler ) {
addRouteWithMetrics := func ( pattern string , handlerLabel string , h http . Handler ) {
@ -197,18 +277,18 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
return allowed
return allowed
}
}
// replaceAdmin replaces the running admin server according
// replaceLocal AdminServer replaces the running local admin server
// to the relevant configuration in cfg. If no configuration
// according to the relevant configuration in cfg. If no configuration
// for the admin endpoint exists in cfg, a default one is
// for the admin endpoint exists in cfg, a default one is used, so
// used, so that there is always an admin server (unless it
// that there is always an admin server (unless it is explicitly
// is explicitly configured to be disabled).
// configured to be disabled).
func replaceAdmin ( cfg * Config ) error {
func replaceLocal AdminServer ( cfg * Config ) error {
// always be sure to close down the old admin endpoint
// always be sure to close down the old admin endpoint
// as gracefully as possible, even if the new one is
// as gracefully as possible, even if the new one is
// disabled -- careful to use reference to the current
// disabled -- careful to use reference to the current
// (old) admin endpoint since it will be different
// (old) admin endpoint since it will be different
// when the function returns
// when the function returns
oldAdminServer := adminServer
oldAdminServer := loc alA dminServer
defer func ( ) {
defer func ( ) {
// do the shutdown asynchronously so that any
// do the shutdown asynchronously so that any
// current API request gets a response; this
// current API request gets a response; this
@ -236,19 +316,20 @@ func replaceAdmin(cfg *Config) error {
}
}
// extract a singular listener address
// extract a singular listener address
addr , err := adminConfig . listenAddr ( )
addr , err := parseAdminListenAddr ( adminConfig . Listen , DefaultAdminListen )
if err != nil {
if err != nil {
return err
return err
}
}
handler := adminConfig . newAdminHandler ( addr )
handler := adminConfig . newAdminHandler ( addr , false )
ln , err := Listen ( addr . Network , addr . JoinHostPort ( 0 ) )
ln , err := Listen ( addr . Network , addr . JoinHostPort ( 0 ) )
if err != nil {
if err != nil {
return err
return err
}
}
adminServer = & http . Server {
localAdminServer = & http . Server {
Addr : addr . String ( ) , // for logging purposes only
Handler : handler ,
Handler : handler ,
ReadTimeout : 10 * time . Second ,
ReadTimeout : 10 * time . Second ,
ReadHeaderTimeout : 5 * time . Second ,
ReadHeaderTimeout : 5 * time . Second ,
@ -258,7 +339,7 @@ func replaceAdmin(cfg *Config) error {
adminLogger := Log ( ) . Named ( "admin" )
adminLogger := Log ( ) . Named ( "admin" )
go func ( ) {
go func ( ) {
if err := adminServer . Serve ( ln ) ; ! errors . Is ( err , http . ErrServerClosed ) {
if err := loc alA dminServer. Serve ( ln ) ; ! errors . Is ( err , http . ErrServerClosed ) {
adminLogger . Error ( "admin server shutdown for unknown reason" , zap . Error ( err ) )
adminLogger . Error ( "admin server shutdown for unknown reason" , zap . Error ( err ) )
}
}
} ( )
} ( )
@ -276,6 +357,252 @@ func replaceAdmin(cfg *Config) error {
return nil
return nil
}
}
// manageIdentity sets up automated identity management for this server.
func manageIdentity ( ctx Context , cfg * Config ) error {
if cfg == nil || cfg . Admin == nil || cfg . Admin . Identity == nil {
return nil
}
oldIdentityCertCache := identityCertCache
if oldIdentityCertCache != nil {
defer oldIdentityCertCache . Stop ( )
}
// set default issuers; this is pretty hacky because we can't
// import the caddytls package -- but it works
if cfg . Admin . Identity . IssuersRaw == nil {
cfg . Admin . Identity . IssuersRaw = [ ] json . RawMessage {
json . RawMessage ( ` { "module": "zerossl"} ` ) ,
json . RawMessage ( ` { "module": "acme"} ` ) ,
}
}
// load and provision issuer modules
if cfg . Admin . Identity . IssuersRaw != nil {
val , err := ctx . LoadModule ( cfg . Admin . Identity , "IssuersRaw" )
if err != nil {
return fmt . Errorf ( "loading identity issuer modules: %s" , err )
}
for _ , issVal := range val . ( [ ] interface { } ) {
cfg . Admin . Identity . issuers = append ( cfg . Admin . Identity . issuers , issVal . ( certmagic . Issuer ) )
}
}
logger := Log ( ) . Named ( "admin.identity" )
cmCfg := cfg . Admin . Identity . certmagicConfig ( logger )
// issuers have circular dependencies with the configs because,
// as explained in the caddytls package, they need access to the
// correct storage and cache to solve ACME challenges
for _ , issuer := range cfg . Admin . Identity . issuers {
// avoid import cycle with caddytls package, so manually duplicate the interface here, yuck
if annoying , ok := issuer . ( interface { SetConfig ( cfg * certmagic . Config ) } ) ; ok {
annoying . SetConfig ( cmCfg )
}
}
// obtain and renew server identity certificate(s)
return cmCfg . ManageAsync ( ctx , cfg . Admin . Identity . Identifiers )
}
// replaceRemoteAdminServer replaces the running remote admin server
// according to the relevant configuration in cfg. It stops any previous
// remote admin server and only starts a new one if configured.
func replaceRemoteAdminServer ( ctx Context , cfg * Config ) error {
if cfg == nil {
return nil
}
remoteLogger := Log ( ) . Named ( "admin.remote" )
oldAdminServer := remoteAdminServer
defer func ( ) {
if oldAdminServer != nil {
go func ( oldAdminServer * http . Server ) {
err := stopAdminServer ( oldAdminServer )
if err != nil {
Log ( ) . Named ( "admin" ) . Error ( "stopping current secure admin endpoint" , zap . Error ( err ) )
}
} ( oldAdminServer )
}
} ( )
if cfg . Admin == nil || cfg . Admin . Remote == nil {
return nil
}
addr , err := parseAdminListenAddr ( cfg . Admin . Remote . Listen , DefaultRemoteAdminListen )
if err != nil {
return err
}
// make the HTTP handler but disable Host/Origin enforcement
// because we are using TLS authentication instead
handler := cfg . Admin . newAdminHandler ( addr , true )
// create client certificate pool for TLS mutual auth, and extract public keys
// so that we can enforce access controls at the application layer
clientCertPool := x509 . NewCertPool ( )
for i , accessControl := range cfg . Admin . Remote . AccessControl {
for j , certBase64 := range accessControl . PublicKeys {
cert , err := decodeBase64DERCert ( certBase64 )
if err != nil {
return fmt . Errorf ( "access control %d public key %d: parsing base64 certificate DER: %v" , i , j , err )
}
accessControl . publicKeys = append ( accessControl . publicKeys , cert . PublicKey )
clientCertPool . AddCert ( cert )
}
}
// create TLS config that will enforce mutual authentication
cmCfg := cfg . Admin . Identity . certmagicConfig ( remoteLogger )
tlsConfig := cmCfg . TLSConfig ( )
tlsConfig . NextProtos = nil // this server does not solve ACME challenges
tlsConfig . ClientAuth = tls . RequireAndVerifyClientCert
tlsConfig . ClientCAs = clientCertPool
// convert logger to stdlib so it can be used by HTTP server
serverLogger , err := zap . NewStdLogAt ( remoteLogger , zap . DebugLevel )
if err != nil {
return err
}
// create secure HTTP server
remoteAdminServer = & http . Server {
Addr : addr . String ( ) , // for logging purposes only
Handler : handler ,
TLSConfig : tlsConfig ,
ReadTimeout : 10 * time . Second ,
ReadHeaderTimeout : 5 * time . Second ,
IdleTimeout : 60 * time . Second ,
MaxHeaderBytes : 1024 * 64 ,
ErrorLog : serverLogger ,
}
// start listener
ln , err := Listen ( addr . Network , addr . JoinHostPort ( 0 ) )
if err != nil {
return err
}
ln = tls . NewListener ( ln , tlsConfig )
go func ( ) {
if err := remoteAdminServer . Serve ( ln ) ; ! errors . Is ( err , http . ErrServerClosed ) {
remoteLogger . Error ( "admin remote server shutdown for unknown reason" , zap . Error ( err ) )
}
} ( )
remoteLogger . Info ( "secure admin remote control endpoint started" ,
zap . String ( "address" , addr . String ( ) ) )
return nil
}
func ( ident * IdentityConfig ) certmagicConfig ( logger * zap . Logger ) * certmagic . Config {
if ident == nil {
// user might not have configured identity; that's OK, we can still make a
// certmagic config, although it'll be mostly useless for remote management
ident = new ( IdentityConfig )
}
cmCfg := & certmagic . Config {
Storage : DefaultStorage , // do not act as part of a cluster (this is for the server's local identity)
Logger : logger ,
Issuers : ident . issuers ,
}
if identityCertCache == nil {
identityCertCache = certmagic . NewCache ( certmagic . CacheOptions {
GetConfigForCert : func ( certmagic . Certificate ) ( * certmagic . Config , error ) {
return cmCfg , nil
} ,
} )
}
return certmagic . New ( identityCertCache , * cmCfg )
}
// IdentityCredentials returns this instance's configured, managed identity credentials
// that can be used in TLS client authentication.
func ( ctx Context ) IdentityCredentials ( logger * zap . Logger ) ( [ ] tls . Certificate , error ) {
if ctx . cfg == nil || ctx . cfg . Admin == nil || ctx . cfg . Admin . Identity == nil {
return nil , fmt . Errorf ( "no server identity configured" )
}
ident := ctx . cfg . Admin . Identity
if len ( ident . Identifiers ) == 0 {
return nil , fmt . Errorf ( "no identifiers configured" )
}
if logger == nil {
logger = Log ( )
}
magic := ident . certmagicConfig ( logger )
return magic . ClientCredentials ( ctx , ident . Identifiers )
}
// enforceAccessControls enforces application-layer access controls for r based on remote.
// It expects that the TLS server has already established at least one verified chain of
// trust, and then looks for a matching, authorized public key that is allowed to access
// the defined path(s) using the defined method(s).
func ( remote RemoteAdmin ) enforceAccessControls ( r * http . Request ) error {
for _ , chain := range r . TLS . VerifiedChains {
for _ , peerCert := range chain {
for _ , adminAccess := range remote . AccessControl {
for _ , allowedKey := range adminAccess . publicKeys {
// see if we found a matching public key; the TLS server already verified the chain
// so we know the client possesses the associated private key; this handy interface
// doesn't appear to be defined anywhere in the std lib, but was implemented here:
// https://github.com/golang/go/commit/b5f2c0f50297fa5cd14af668ddd7fd923626cf8c
comparer , ok := peerCert . PublicKey . ( interface { Equal ( crypto . PublicKey ) bool } )
if ! ok || ! comparer . Equal ( allowedKey ) {
continue
}
// key recognized; make sure its HTTP request is permitted
for _ , accessPerm := range adminAccess . Permissions {
// verify method
methodFound := accessPerm . Methods == nil
for _ , method := range accessPerm . Methods {
if method == r . Method {
methodFound = true
break
}
}
if ! methodFound {
return APIError {
HTTPStatus : http . StatusForbidden ,
Message : "not authorized to use this method" ,
}
}
// verify path
pathFound := accessPerm . Paths == nil
for _ , allowedPath := range accessPerm . Paths {
if strings . HasPrefix ( r . URL . Path , allowedPath ) {
pathFound = true
break
}
}
if ! pathFound {
return APIError {
HTTPStatus : http . StatusForbidden ,
Message : "not authorized to access this path" ,
}
}
}
// public key authorized, method and path allowed
return nil
}
}
}
}
// in theory, this should never happen; with an unverified chain, the TLS server
// should not accept the connection in the first place, and the acceptable cert
// pool is configured using the same list of public keys we verify against
return APIError {
HTTPStatus : http . StatusUnauthorized ,
Message : "client identity not authorized" ,
}
}
func stopAdminServer ( srv * http . Server ) error {
func stopAdminServer ( srv * http . Server ) error {
if srv == nil {
if srv == nil {
return fmt . Errorf ( "no admin server" )
return fmt . Errorf ( "no admin server" )
@ -286,7 +613,7 @@ func stopAdminServer(srv *http.Server) error {
if err != nil {
if err != nil {
return fmt . Errorf ( "shutting down admin server: %v" , err )
return fmt . Errorf ( "shutting down admin server: %v" , err )
}
}
Log ( ) . Named ( "admin" ) . Info ( "stopped previous server" )
Log ( ) . Named ( "admin" ) . Info ( "stopped previous server" , zap . String ( "address" , srv . Addr ) )
return nil
return nil
}
}
@ -302,10 +629,15 @@ type AdminRoute struct {
}
}
type adminHandler struct {
type adminHandler struct {
mux * http . ServeMux
// security for local/plaintext) endpoint, on by default
enforceOrigin bool
enforceOrigin bool
enforceHost bool
enforceHost bool
allowedOrigins [ ] string
allowedOrigins [ ] string
mux * http . ServeMux
// security for remote/encrypted endpoint
remoteControl * RemoteAdmin
}
}
// ServeHTTP is the external entry point for API requests.
// ServeHTTP is the external entry point for API requests.
@ -318,6 +650,12 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
zap . String ( "remote_addr" , r . RemoteAddr ) ,
zap . String ( "remote_addr" , r . RemoteAddr ) ,
zap . Reflect ( "headers" , r . Header ) ,
zap . Reflect ( "headers" , r . Header ) ,
)
)
if r . TLS != nil {
log = log . With (
zap . Bool ( "secure" , true ) ,
zap . Int ( "verified_chains" , len ( r . TLS . VerifiedChains ) ) ,
)
}
if r . RequestURI == "/metrics" {
if r . RequestURI == "/metrics" {
log . Debug ( "received request" )
log . Debug ( "received request" )
} else {
} else {
@ -330,6 +668,14 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// be called more than once per request, for example if a request
// be called more than once per request, for example if a request
// is rewritten (i.e. internal redirect).
// is rewritten (i.e. internal redirect).
func ( h adminHandler ) serveHTTP ( w http . ResponseWriter , r * http . Request ) {
func ( h adminHandler ) serveHTTP ( w http . ResponseWriter , r * http . Request ) {
if h . remoteControl != nil {
// enforce access controls on secure endpoint
if err := h . remoteControl . enforceAccessControls ( r ) ; err != nil {
h . handleError ( w , r , err )
return
}
}
if strings . Contains ( r . Header . Get ( "Upgrade" ) , "websocket" ) {
if strings . Contains ( r . Header . Get ( "Upgrade" ) , "websocket" ) {
// I've never been able demonstrate a vulnerability myself, but apparently
// I've never been able demonstrate a vulnerability myself, but apparently
// WebSocket connections originating from browsers aren't subject to CORS
// WebSocket connections originating from browsers aren't subject to CORS
@ -363,8 +709,6 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
w . Header ( ) . Set ( "Access-Control-Allow-Origin" , origin )
w . Header ( ) . Set ( "Access-Control-Allow-Origin" , origin )
}
}
// TODO: authentication & authorization, if configured
h . mux . ServeHTTP ( w , r )
h . mux . ServeHTTP ( w , r )
}
}
@ -372,20 +716,16 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
if err == nil {
if err == nil {
return
return
}
}
if err == ErrInternalRedir {
h . serveHTTP ( w , r )
return
}
apiErr , ok := err . ( APIError )
apiErr , ok := err . ( APIError )
if ! ok {
if ! ok {
apiErr = APIError {
apiErr = APIError {
Code : http . StatusInternalServerError ,
HTTPStatus : http . StatusInternalServerError ,
Err : err ,
Err : err ,
}
}
}
}
if apiErr . Code == 0 {
if apiErr . HTTPStatus == 0 {
apiErr . Code = http . StatusInternalServerError
apiErr . HTTPStatus = http . StatusInternalServerError
}
}
if apiErr . Message == "" && apiErr . Err != nil {
if apiErr . Message == "" && apiErr . Err != nil {
apiErr . Message = apiErr . Err . Error ( )
apiErr . Message = apiErr . Err . Error ( )
@ -393,11 +733,11 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
Log ( ) . Named ( "admin.api" ) . Error ( "request error" ,
Log ( ) . Named ( "admin.api" ) . Error ( "request error" ,
zap . Error ( err ) ,
zap . Error ( err ) ,
zap . Int ( "status_code" , apiErr . Code ) ,
zap . Int ( "status_code" , apiErr . HTTPStatus ) ,
)
)
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . WriteHeader ( apiErr . Code )
w . WriteHeader ( apiErr . HTTPStatus )
encErr := json . NewEncoder ( w ) . Encode ( apiErr )
encErr := json . NewEncoder ( w ) . Encode ( apiErr )
if encErr != nil {
if encErr != nil {
Log ( ) . Named ( "admin.api" ) . Error ( "failed to encode error response" , zap . Error ( encErr ) )
Log ( ) . Named ( "admin.api" ) . Error ( "failed to encode error response" , zap . Error ( encErr ) )
@ -418,7 +758,7 @@ func (h adminHandler) checkHost(r *http.Request) error {
}
}
if ! allowed {
if ! allowed {
return APIError {
return APIError {
Code : http . StatusForbidden ,
HTTPStatus : http . StatusForbidden ,
Err : fmt . Errorf ( "host not allowed: %s" , r . Host ) ,
Err : fmt . Errorf ( "host not allowed: %s" , r . Host ) ,
}
}
}
}
@ -433,13 +773,13 @@ func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
origin := h . getOriginHost ( r )
origin := h . getOriginHost ( r )
if origin == "" {
if origin == "" {
return origin , APIError {
return origin , APIError {
Code : http . StatusForbidden ,
HTTPStatus : http . StatusForbidden ,
Err : fmt . Errorf ( "missing required Origin header" ) ,
Err : fmt . Errorf ( "missing required Origin header" ) ,
}
}
}
}
if ! h . originAllowed ( origin ) {
if ! h . originAllowed ( origin ) {
return origin , APIError {
return origin , APIError {
Code : http . StatusForbidden ,
HTTPStatus : http . StatusForbidden ,
Err : fmt . Errorf ( "client is not allowed to access from origin %s" , origin ) ,
Err : fmt . Errorf ( "client is not allowed to access from origin %s" , origin ) ,
}
}
}
}
@ -480,7 +820,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
err := readConfig ( r . URL . Path , w )
err := readConfig ( r . URL . Path , w )
if err != nil {
if err != nil {
return APIError { Code : http . StatusBadRequest , Err : err }
return APIError { HTTPStatus : http . StatusBadRequest , Err : err }
}
}
return nil
return nil
@ -495,7 +835,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
if r . Method != http . MethodDelete {
if r . Method != http . MethodDelete {
if ct := r . Header . Get ( "Content-Type" ) ; ! strings . Contains ( ct , "/json" ) {
if ct := r . Header . Get ( "Content-Type" ) ; ! strings . Contains ( ct , "/json" ) {
return APIError {
return APIError {
Code : http . StatusBadRequest ,
HTTPStatus : http . StatusBadRequest ,
Err : fmt . Errorf ( "unacceptable content-type: %v; 'application/json' required" , ct ) ,
Err : fmt . Errorf ( "unacceptable content-type: %v; 'application/json' required" , ct ) ,
}
}
}
}
@ -507,7 +847,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
_ , err := io . Copy ( buf , r . Body )
_ , err := io . Copy ( buf , r . Body )
if err != nil {
if err != nil {
return APIError {
return APIError {
Code : http . StatusBadRequest ,
HTTPStatus : http . StatusBadRequest ,
Err : fmt . Errorf ( "reading request body: %v" , err ) ,
Err : fmt . Errorf ( "reading request body: %v" , err ) ,
}
}
}
}
@ -523,7 +863,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
default :
default :
return APIError {
return APIError {
Code : http . StatusMethodNotAllowed ,
HTTPStatus : http . StatusMethodNotAllowed ,
Err : fmt . Errorf ( "method %s not allowed" , r . Method ) ,
Err : fmt . Errorf ( "method %s not allowed" , r . Method ) ,
}
}
}
}
@ -555,46 +895,17 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
parts = append ( [ ] string { expanded } , parts [ 3 : ] ... )
parts = append ( [ ] string { expanded } , parts [ 3 : ] ... )
r . URL . Path = path . Join ( parts ... )
r . URL . Path = path . Join ( parts ... )
return ErrInternalRedir
}
func handleStop ( w http . ResponseWriter , r * http . Request ) error {
err := handleUnload ( w , r )
if err != nil {
Log ( ) . Named ( "admin.api" ) . Error ( "unload error" , zap . Error ( err ) )
}
if adminServer != nil {
// use goroutine so that we can finish responding to API request
go func ( ) {
err := stopAdminServer ( adminServer )
var exitCode int
if err != nil {
exitCode = ExitCodeFailedQuit
Log ( ) . Named ( "admin.api" ) . Error ( "failed to stop admin server gracefully" , zap . Error ( err ) )
}
Log ( ) . Named ( "admin.api" ) . Info ( "stopping now, bye!! 👋" )
os . Exit ( exitCode )
} ( )
}
return nil
return nil
}
}
// handleUnload stops the current configuration that is running.
func handleStop ( w http . ResponseWriter , r * http . Request ) error {
// Note that doing this can also be accomplished with DELETE /config/
// but we leave this function because handleStop uses it.
func handleUnload ( w http . ResponseWriter , r * http . Request ) error {
if r . Method != http . MethodPost {
if r . Method != http . MethodPost {
return APIError {
return APIError {
Code : http . StatusMethodNotAllowed ,
HTTPStatus : http . StatusMethodNotAllowed ,
Err : fmt . Errorf ( "method not allowed" ) ,
Err : fmt . Errorf ( "method not allowed" ) ,
}
}
}
}
Log ( ) . Named ( "admin.api" ) . Info ( "unloading" )
exitProcess ( Log ( ) . Named ( "admin.api" ) )
if err := stopAndCleanup ( ) ; err != nil {
Log ( ) . Named ( "admin.api" ) . Error ( "error unloading" , zap . Error ( err ) )
} else {
Log ( ) . Named ( "admin.api" ) . Info ( "unloading completed" )
}
return nil
return nil
}
}
@ -806,7 +1117,7 @@ func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) erro
// and client responses. If Message is unset, then
// and client responses. If Message is unset, then
// Err.Error() will be serialized in its place.
// Err.Error() will be serialized in its place.
type APIError struct {
type APIError struct {
Code int ` json:"-" `
HTTPStatus int ` json:"-" `
Err error ` json:"-" `
Err error ` json:"-" `
Message string ` json:"error" `
Message string ` json:"error" `
}
}
@ -818,20 +1129,44 @@ func (e APIError) Error() string {
return e . Message
return e . Message
}
}
// parseAdminListenAddr extracts a singular listen address from either addr
// or defaultAddr, returning the network and the address of the listener.
func parseAdminListenAddr ( addr string , defaultAddr string ) ( NetworkAddress , error ) {
input := addr
if input == "" {
input = defaultAddr
}
listenAddr , err := ParseNetworkAddress ( input )
if err != nil {
return NetworkAddress { } , fmt . Errorf ( "parsing listener address: %v" , err )
}
if listenAddr . PortRangeSize ( ) != 1 {
return NetworkAddress { } , fmt . Errorf ( "must be exactly one listener address; cannot listen on: %s" , listenAddr )
}
return listenAddr , nil
}
// decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
func decodeBase64DERCert ( certStr string ) ( * x509 . Certificate , error ) {
derBytes , err := base64 . StdEncoding . DecodeString ( certStr )
if err != nil {
return nil , err
}
return x509 . ParseCertificate ( derBytes )
}
var (
var (
// DefaultAdminListen is the address for the admin
// DefaultAdminListen is the address for the local admin
// listener, if none is specified at startup.
// listener, if none is specified at startup.
DefaultAdminListen = "localhost:2019"
DefaultAdminListen = "localhost:2019"
// ErrInternalRedir indicates an internal redirect
// DefaultRemoteAdminListen is the address for the remote
// and is useful when admin API handlers rewrite
// (TLS-authenticated) admin listener, if enabled and not
// the request; in that case, authentication and
// specified otherwise.
// authorization needs to happen again for the
DefaultRemoteAdminListen = ":2021"
// rewritten request.
ErrInternalRedir = fmt . Errorf ( "internal redirect; re-authorization required" )
// DefaultAdminConfig is the default configuration
// DefaultAdminConfig is the default configuration
// for the administration endpoint.
// for the local administration endpoint.
DefaultAdminConfig = & AdminConfig {
DefaultAdminConfig = & AdminConfig {
Listen : DefaultAdminListen ,
Listen : DefaultAdminListen ,
}
}
@ -869,4 +1204,8 @@ var bufPool = sync.Pool{
} ,
} ,
}
}
var adminServer * http . Server
// keep a reference to admin endpoint singletons while they're active
var (
localAdminServer , remoteAdminServer * http . Server
identityCertCache * certmagic . Cache
)