@marcel91200 / caddy Mirror · read-only / commits / 996af091
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 14, 05:15 UTC

cmd: Support admin endpoint on unix socket (#3320)

Matt Holt committed May 29, 2020 at 14:21 UTC 996af0915dfc597fc6aad82e99578fed58d0f87d
1 file changed +61 -33
cmd/commandfuncs.go
+61 -33
@@ -16,6 +16,7 @@ package caddycmd
16
17 import (
18 "bytes"
19 + "context"
20 "crypto/rand"
21 "encoding/json"
22 "fmt"
@@ -276,24 +277,9 @@ func cmdRun(fl Flags) (int, error) {
277 func cmdStop(fl Flags) (int, error) {
278 stopCmdAddrFlag := fl.String("address")
279
279 - adminAddr := caddy.DefaultAdminListen
280 - if stopCmdAddrFlag != "" {
281 - adminAddr = stopCmdAddrFlag
282 - }
283 - stopEndpoint := fmt.Sprintf("http://%s/stop", adminAddr)
284 -
285 - req, err := http.NewRequest(http.MethodPost, stopEndpoint, nil)
286 - if err != nil {
287 - return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
288 - }
289 - req.Header.Set("Origin", adminAddr)
290 -
291 - err = apiRequest(req)
280 + err := apiRequest(stopCmdAddrFlag, http.MethodPost, "/stop", nil)
281 if err != nil {
293 - caddy.Log().Warn("failed using API to stop instance",
294 - zap.String("endpoint", stopEndpoint),
295 - zap.Error(err),
296 - )
282 + caddy.Log().Warn("failed using API to stop instance", zap.Error(err))
283 return caddy.ExitCodeFailedStartup, err
284 }
285
@@ -314,7 +300,7 @@ func cmdReload(fl Flags) (int, error) {
300 return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
301 }
302
317 - // get the address of the admin listener and craft endpoint URL
303 + // get the address of the admin listener; use flag if specified
304 adminAddr := reloadCmdAddrFlag
305 if adminAddr == "" && len(config) > 0 {
306 var tmpStruct struct {
@@ -327,20 +313,8 @@ func cmdReload(fl Flags) (int, error) {
313 }
314 adminAddr = tmpStruct.Admin.Listen
315 }
330 - if adminAddr == "" {
331 - adminAddr = caddy.DefaultAdminListen
332 - }
333 - loadEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
334 -
335 - // prepare the request to update the configuration
336 - req, err := http.NewRequest(http.MethodPost, loadEndpoint, bytes.NewReader(config))
337 - if err != nil {
338 - return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
339 - }
340 - req.Header.Set("Content-Type", "application/json")
341 - req.Header.Set("Origin", adminAddr)
316
343 - err = apiRequest(req)
317 + err = apiRequest(adminAddr, http.MethodPost, "/load", bytes.NewReader(config))
318 if err != nil {
319 return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
320 }
@@ -645,8 +619,62 @@ commands:
619 return caddy.ExitCodeSuccess, nil
620 }
621
648 -func apiRequest(req *http.Request) error {
649 - resp, err := http.DefaultClient.Do(req)
622 +// apiRequest makes an API request to the endpoint adminAddr with the
623 +// given HTTP method and request URI. If body is non-nil, it will be
624 +// assumed to be Content-Type application/json.
625 +func apiRequest(adminAddr, method, uri string, body io.Reader) error {
626 + // parse the admin address
627 + if adminAddr == "" {
628 + adminAddr = caddy.DefaultAdminListen
629 + }
630 + parsedAddr, err := caddy.ParseNetworkAddress(adminAddr)
631 + if err != nil || parsedAddr.PortRangeSize() > 1 {
632 + return fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
633 + }
634 + origin := parsedAddr.JoinHostPort(0)
635 + if parsedAddr.IsUnixNetwork() {
636 + origin = "unixsocket" // hack so that http.NewRequest() is happy
637 + }
638 +
639 + // form the request
640 + req, err := http.NewRequest(method, "http://"+origin+uri, body)
641 + if err != nil {
642 + return fmt.Errorf("making request: %v", err)
643 + }
644 + if parsedAddr.IsUnixNetwork() {
645 + // When listening on a unix socket, the admin endpoint doesn't
646 + // accept any Host header because there is no host:port for
647 + // a unix socket's address. The server's host check is fairly
648 + // strict for security reasons, so we don't allow just any
649 + // Host header. For unix sockets, the Host header must be
650 + // empty. Unfortunately, Go makes it impossible to make HTTP
651 + // requests with an empty Host header... except with this one
652 + // weird trick. (Hopefully they don't fix it. It's already
653 + // hard enough to use HTTP over unix sockets.)
654 + //
655 + // An equivalent curl command would be something like:
656 + // $ curl --unix-socket caddy.sock http:/:$REQUEST_URI
657 + req.URL.Host = " "
658 + req.Host = ""
659 + } else {
660 + req.Header.Set("Origin", origin)
661 + }
662 + if body != nil {
663 + req.Header.Set("Content-Type", "application/json")
664 + }
665 +
666 + // make an HTTP client that dials our network type, since admin
667 + // endpoints aren't always TCP, which is what the default transport
668 + // expects; reuse is not of particular concern here
669 + client := http.Client{
670 + Transport: &http.Transport{
671 + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
672 + return net.Dial(parsedAddr.Network, parsedAddr.JoinHostPort(0))
673 + },
674 + },
675 + }
676 +
677 + resp, err := client.Do(req)
678 if err != nil {
679 return fmt.Errorf("performing request: %v", err)
680 }