From 996af0915dfc597fc6aad82e99578fed58d0f87d Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Fri, 29 May 2020 14:21:55 -0600 Subject: [PATCH] cmd: Support admin endpoint on unix socket (#3320) --- cmd/commandfuncs.go | 94 +++++++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 875fb5a9..a10bc191 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -16,6 +16,7 @@ package caddycmd import ( "bytes" + "context" "crypto/rand" "encoding/json" "fmt" @@ -276,24 +277,9 @@ func cmdRun(fl Flags) (int, error) { func cmdStop(fl Flags) (int, error) { stopCmdAddrFlag := fl.String("address") - adminAddr := caddy.DefaultAdminListen - if stopCmdAddrFlag != "" { - adminAddr = stopCmdAddrFlag - } - stopEndpoint := fmt.Sprintf("http://%s/stop", adminAddr) - - req, err := http.NewRequest(http.MethodPost, stopEndpoint, nil) + err := apiRequest(stopCmdAddrFlag, http.MethodPost, "/stop", nil) if err != nil { - return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err) - } - req.Header.Set("Origin", adminAddr) - - err = apiRequest(req) - if err != nil { - caddy.Log().Warn("failed using API to stop instance", - zap.String("endpoint", stopEndpoint), - zap.Error(err), - ) + caddy.Log().Warn("failed using API to stop instance", zap.Error(err)) return caddy.ExitCodeFailedStartup, err } @@ -314,7 +300,7 @@ func cmdReload(fl Flags) (int, error) { return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load") } - // get the address of the admin listener and craft endpoint URL + // get the address of the admin listener; use flag if specified adminAddr := reloadCmdAddrFlag if adminAddr == "" && len(config) > 0 { var tmpStruct struct { @@ -327,20 +313,8 @@ func cmdReload(fl Flags) (int, error) { } adminAddr = tmpStruct.Admin.Listen } - if adminAddr == "" { - adminAddr = caddy.DefaultAdminListen - } - loadEndpoint := fmt.Sprintf("http://%s/load", adminAddr) - // prepare the request to update the configuration - req, err := http.NewRequest(http.MethodPost, loadEndpoint, bytes.NewReader(config)) - if err != nil { - return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Origin", adminAddr) - - err = apiRequest(req) + err = apiRequest(adminAddr, http.MethodPost, "/load", bytes.NewReader(config)) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err) } @@ -645,8 +619,62 @@ commands: return caddy.ExitCodeSuccess, nil } -func apiRequest(req *http.Request) error { - resp, err := http.DefaultClient.Do(req) +// 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, body io.Reader) error { + // parse the admin address + if adminAddr == "" { + adminAddr = caddy.DefaultAdminListen + } + parsedAddr, err := caddy.ParseNetworkAddress(adminAddr) + if err != nil || parsedAddr.PortRangeSize() > 1 { + return fmt.Errorf("invalid admin address %s: %v", adminAddr, err) + } + origin := parsedAddr.JoinHostPort(0) + if parsedAddr.IsUnixNetwork() { + origin = "unixsocket" // hack so that http.NewRequest() is happy + } + + // form the request + req, err := http.NewRequest(method, "http://"+origin+uri, body) + if err != nil { + return fmt.Errorf("making request: %v", err) + } + if parsedAddr.IsUnixNetwork() { + // When listening on a unix socket, the admin endpoint doesn't + // accept any Host header because there is no host:port for + // a unix socket's address. The server's host check is fairly + // strict for security reasons, so we don't allow just any + // Host header. For unix sockets, the Host header must be + // empty. Unfortunately, Go makes it impossible to make HTTP + // requests with an empty Host header... except with this one + // weird trick. (Hopefully they don't fix it. It's already + // hard enough to use HTTP over unix sockets.) + // + // An equivalent curl command would be something like: + // $ curl --unix-socket caddy.sock http:/:$REQUEST_URI + req.URL.Host = " " + req.Host = "" + } else { + req.Header.Set("Origin", origin) + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + // make an HTTP client that dials our network type, since admin + // endpoints aren't always TCP, which is what the default transport + // expects; reuse is not of particular concern here + client := http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial(parsedAddr.Network, parsedAddr.JoinHostPort(0)) + }, + }, + } + + resp, err := client.Do(req) if err != nil { return fmt.Errorf("performing request: %v", err) }