From bbad6931e30a2e74b3f53fff797d1115cc9dd491 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Wed, 2 Mar 2022 13:08:36 -0500 Subject: pki: Implement API endpoints for certs and `caddy trust` (#4443) * admin: Implement /pki/certificates/ API * pki: Lower "skip_install_trust" log level to INFO See https://github.com/caddyserver/caddy/issues/4058#issuecomment-976132935 It's not necessary to warn about this, because this was an option explicitly configured by the user. Still useful to log, but we don't need to be so loud about it. * cmd: Export functions needed for PKI app, return API response to caller * pki: Rewrite `caddy trust` command to use new admin endpoint instead * pki: Rewrite `caddy untrust` command to support using admin endpoint * Refactor cmd and pki packages for determining admin API endpoint --- cmd/commandfuncs.go | 112 ++++++++++++++++++++++++++++++++++------------------ cmd/commands.go | 7 +++- cmd/main.go | 8 ++-- 3 files changed, 83 insertions(+), 44 deletions(-) (limited to 'cmd') diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index ec56ab9..d308aeb 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -202,7 +202,7 @@ func cmdRun(fl Flags) (int, error) { // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive var configFile string if !runCmdResumeFlag { - config, configFile, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag) + config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } @@ -275,25 +275,33 @@ func cmdRun(fl Flags) (int, error) { } func cmdStop(fl Flags) (int, error) { - stopCmdAddrFlag := fl.String("address") + addrFlag := fl.String("address") + configFlag := fl.String("config") + configAdapterFlag := fl.String("adapter") - err := apiRequest(stopCmdAddrFlag, http.MethodPost, "/stop", nil, nil) + adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) + } + + resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/stop", nil, nil) if err != nil { caddy.Log().Warn("failed using API to stop instance", zap.Error(err)) return caddy.ExitCodeFailedStartup, err } + defer resp.Body.Close() return caddy.ExitCodeSuccess, nil } func cmdReload(fl Flags) (int, error) { - reloadCmdConfigFlag := fl.String("config") - reloadCmdConfigAdapterFlag := fl.String("adapter") - reloadCmdAddrFlag := fl.String("address") - reloadCmdForceFlag := fl.Bool("force") + configFlag := fl.String("config") + configAdapterFlag := fl.String("adapter") + addrFlag := fl.String("address") + forceFlag := fl.Bool("force") // get the config in caddy's native format - config, configFile, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag) + config, configFile, err := LoadConfig(configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } @@ -301,30 +309,22 @@ func cmdReload(fl Flags) (int, error) { return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load") } - // get the address of the admin listener; use flag if specified - adminAddr := reloadCmdAddrFlag - if adminAddr == "" && len(config) > 0 { - var tmpStruct struct { - Admin caddy.AdminConfig `json:"admin"` - } - err = json.Unmarshal(config, &tmpStruct) - if err != nil { - return caddy.ExitCodeFailedStartup, - fmt.Errorf("unmarshaling admin listener address from config: %v", err) - } - adminAddr = tmpStruct.Admin.Listen + adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } // optionally force a config reload headers := make(http.Header) - if reloadCmdForceFlag { + if forceFlag { headers.Set("Cache-Control", "must-revalidate") } - err = apiRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config)) + resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config)) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err) } + defer resp.Body.Close() return caddy.ExitCodeSuccess, nil } @@ -518,7 +518,7 @@ func cmdValidateConfig(fl Flags) (int, error) { validateCmdConfigFlag := fl.String("config") validateCmdAdapterFlag := fl.String("adapter") - input, _, err := loadConfig(validateCmdConfigFlag, validateCmdAdapterFlag) + input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } @@ -640,17 +640,15 @@ commands: return caddy.ExitCodeSuccess, nil } -// apiRequest makes an API request to the endpoint adminAddr with the -// given HTTP method and request URI. If body is non-nil, it will be -// assumed to be Content-Type application/json. -func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) error { - // parse the admin address - if adminAddr == "" { - adminAddr = caddy.DefaultAdminListen - } +// AdminAPIRequest makes an API request according to the CLI flags given, +// with the given HTTP method and request URI. If body is non-nil, it will +// be assumed to be Content-Type application/json. The caller should close +// the response body. Should only be used by Caddy CLI commands which +// need to interact with a running instance of Caddy via the admin API. +func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) (*http.Response, error) { parsedAddr, err := caddy.ParseNetworkAddress(adminAddr) if err != nil || parsedAddr.PortRangeSize() > 1 { - return fmt.Errorf("invalid admin address %s: %v", adminAddr, err) + return nil, fmt.Errorf("invalid admin address %s: %v", adminAddr, err) } origin := parsedAddr.JoinHostPort(0) if parsedAddr.IsUnixNetwork() { @@ -660,7 +658,7 @@ func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Read // form the request req, err := http.NewRequest(method, "http://"+origin+uri, body) if err != nil { - return fmt.Errorf("making request: %v", err) + return nil, fmt.Errorf("making request: %v", err) } if parsedAddr.IsUnixNetwork() { // When listening on a unix socket, the admin endpoint doesn't @@ -700,20 +698,58 @@ func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Read resp, err := client.Do(req) if err != nil { - return fmt.Errorf("performing request: %v", err) + return nil, fmt.Errorf("performing request: %v", err) } - defer resp.Body.Close() // if it didn't work, let the user know if resp.StatusCode >= 400 { respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*10)) if err != nil { - return fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err) + return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err) + } + return nil, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody) + } + + return resp, nil +} + +// DetermineAdminAPIAddress determines which admin API endpoint address should +// be used based on the inputs. By priority: if `address` is specified, then +// it is returned; if `configFile` (and `configAdapter`) are specified, then that +// config will be loaded to find the admin address; otherwise, the default +// admin listen address will be returned. +func DetermineAdminAPIAddress(address, configFile, configAdapter string) (string, error) { + // Prefer the address if specified and non-empty + if address != "" { + return address, nil + } + + // Try to load the config from file if specified, with the given adapter name + if configFile != "" { + // get the config in caddy's native format + config, loadedConfigFile, err := LoadConfig(configFile, configAdapter) + if err != nil { + return "", err + } + if loadedConfigFile == "" { + return "", fmt.Errorf("no config file to load") + } + + // get the address of the admin listener + if len(config) > 0 { + var tmpStruct struct { + Admin caddy.AdminConfig `json:"admin"` + } + err = json.Unmarshal(config, &tmpStruct) + if err != nil { + return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err) + } + return tmpStruct.Admin.Listen, nil } - return fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody) } - return nil + // Fallback to the default listen address otherwise + return caddy.DefaultAdminListen, nil } type moduleInfo struct { diff --git a/cmd/commands.go b/cmd/commands.go index 1e2c40d..0c68b7c 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -156,16 +156,19 @@ development environment.`, RegisterCommand(Command{ Name: "stop", Func: cmdStop, + Usage: "[--address ] [--config [--adapter ]]", Short: "Gracefully stops a started Caddy process", Long: ` Stops the background Caddy process as gracefully as possible. It requires that the admin API is enabled and accessible, since it will -use the API's /stop endpoint. The address of this request can be -customized using the --address flag if it is not the default.`, +use the API's /stop endpoint. The address of this request can be customized +using the --address flag, or from the given --config, if not the default.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("stop", flag.ExitOnError) fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default") + fs.String("config", "", "Configuration file to use to parse the admin address, if --address is not used") + fs.String("adapter", "", "Name of config adapter to apply (when --config is used)") return fs }(), }) diff --git a/cmd/main.go b/cmd/main.go index 7c33c55..f111ba4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -103,15 +103,15 @@ func handlePingbackConn(conn net.Conn, expect []byte) error { return nil } -// loadConfig loads the config from configFile and adapts it +// LoadConfig loads the config from configFile and adapts it // using adapterName. If adapterName is specified, configFile // must be also. If no configFile is specified, it tries // loading a default config file. The lack of a config file is // not treated as an error, but false will be returned if // there is no config available. It prints any warnings to stderr, // and returns the resulting JSON config bytes along with -// whether a config file was loaded or not. -func loadConfig(configFile, adapterName string) ([]byte, string, error) { +// the name of the loaded config file (if any). +func LoadConfig(configFile, adapterName string) ([]byte, string, error) { // specifying an adapter without a config file is ambiguous if adapterName != "" && configFile == "" { return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)") @@ -262,7 +262,7 @@ func watchConfigFile(filename, adapterName string) { lastModified = info.ModTime() // load the contents of the file - config, _, err := loadConfig(filename, adapterName) + config, _, err := LoadConfig(filename, adapterName) if err != nil { logger().Error("unable to load latest config", zap.Error(err)) continue -- cgit v1.2.3