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
}