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
Caddy 2 gets a CLI! And admin endpoint is now configurable via JSON
Matthew Holt committed
Jun 28, 2019 at 15:39 UTC
a4bdf249dbfb0f35e0a35dad6a38db371b126800
8 files changed
+374
-30
admin.go
+48
-3
@@ -22,12 +22,47 @@ var (
22
cfgEndptSrvMu sync.Mutex
23
)
24
25
-// StartAdmin starts Caddy's administration endpoint.
26
-func StartAdmin(addr string) error {
25
+// AdminConfig configures the admin endpoint.
26
+type AdminConfig struct {
27
+ Listen string `json:"listen,omitempty"`
28
+}
29
+
30
+// DefaultAdminConfig is the default configuration
31
+// for the administration endpoint.
32
+var DefaultAdminConfig = &AdminConfig{
33
+ Listen: "localhost:2019",
34
+}
35
+
36
+// StartAdmin starts Caddy's administration endpoint,
37
+// bootstrapping it with an optional configuration
38
+// in the format of JSON bytes. It opens a listener
39
+// resource. When no longer needed, StopAdmin should
40
+// be called.
41
+func StartAdmin(initialConfigJSON []byte) error {
42
cfgEndptSrvMu.Lock()
43
defer cfgEndptSrvMu.Unlock()
44
30
- ln, err := net.Listen("tcp", addr)
45
+ adminConfig := DefaultAdminConfig
46
+ if len(initialConfigJSON) > 0 {
47
+ var config *Config
48
+ err := json.Unmarshal(initialConfigJSON, &config)
49
+ if err != nil {
50
+ return fmt.Errorf("unmarshaling bootstrap config: %v", err)
51
+ }
52
+ if config != nil && config.Admin != nil {
53
+ adminConfig = config.Admin
54
+ }
55
+ if cfgEndptSrv != nil {
56
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
57
+ defer cancel()
58
+ err := cfgEndptSrv.Shutdown(ctx)
59
+ if err != nil {
60
+ return fmt.Errorf("shutting down old admin endpoint: %v", err)
61
+ }
62
+ }
63
+ }
64
+
65
+ ln, err := net.Listen("tcp", adminConfig.Listen)
66
if err != nil {
67
return err
68
}
@@ -60,6 +95,16 @@ func StartAdmin(addr string) error {
95
96
go cfgEndptSrv.Serve(ln)
97
98
+ log.Println("Caddy 2 admin endpoint listening on", adminConfig.Listen)
99
+
100
+ if len(initialConfigJSON) > 0 {
101
+ err := Load(bytes.NewReader(initialConfigJSON))
102
+ if err != nil {
103
+ return fmt.Errorf("loading initial config: %v", err)
104
+ }
105
+ log.Println("Caddy 2 serving initial configuration")
106
+ }
107
+
108
return nil
109
}
110
caddy.go
+2
@@ -136,6 +136,8 @@ type App interface {
136
137
// Config represents a Caddy configuration.
138
type Config struct {
139
+ Admin *AdminConfig `json:"admin,omitempty"`
140
+
141
StorageRaw json.RawMessage `json:"storage,omitempty"`
142
storage certmagic.Storage
143
cmd/caddy/main.go
-1
@@ -5,7 +5,6 @@ import (
5
6
// this is where modules get plugged in
7
_ "github.com/caddyserver/caddy/modules/caddyhttp"
8
- _ "github.com/caddyserver/caddy/modules/caddyhttp/caddylog"
8
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode"
9
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode/brotli"
10
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode/gzip"
cmd/commands.go
new
+205
@@ -0,0 +1,205 @@
1
+package caddycmd
2
+
3
+import (
4
+ "crypto/rand"
5
+ "flag"
6
+ "fmt"
7
+ "io/ioutil"
8
+ "log"
9
+ "net"
10
+ "os"
11
+ "os/exec"
12
+ "path/filepath"
13
+
14
+ "github.com/caddyserver/caddy"
15
+ "github.com/mitchellh/go-ps"
16
+)
17
+
18
+func cmdStart() (int, error) {
19
+ startCmd := flag.NewFlagSet("start", flag.ExitOnError)
20
+ startCmdConfigFlag := startCmd.String("config", "", "Configuration file")
21
+ startCmd.Parse(os.Args[2:])
22
+
23
+ // open a listener to which the child process will connect when
24
+ // it is ready to confirm that it has successfully started
25
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
26
+ if err != nil {
27
+ return 1, fmt.Errorf("opening listener for success confirmation: %v", err)
28
+ }
29
+ defer ln.Close()
30
+
31
+ // craft the command with a pingback address and with a
32
+ // pipe for its stdin, so we can tell it our confirmation
33
+ // code that we expect so that some random port scan at
34
+ // the most unfortunate time won't fool us into thinking
35
+ // the child succeeded (i.e. the alternative is to just
36
+ // wait for any connection on our listener, but better to
37
+ // ensure it's the process we're expecting - we can be
38
+ // sure by giving it some random bytes and having it echo
39
+ // them back to us)
40
+ cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
41
+ if *startCmdConfigFlag != "" {
42
+ cmd.Args = append(cmd.Args, "--config", *startCmdConfigFlag)
43
+ }
44
+ stdinpipe, err := cmd.StdinPipe()
45
+ if err != nil {
46
+ return 1, fmt.Errorf("creating stdin pipe: %v", err)
47
+ }
48
+ cmd.Stdout = os.Stdout
49
+ cmd.Stderr = os.Stderr
50
+
51
+ // generate the random bytes we'll send to the child process
52
+ expect := make([]byte, 32)
53
+ _, err = rand.Read(expect)
54
+ if err != nil {
55
+ return 1, fmt.Errorf("generating random confirmation bytes: %v", err)
56
+ }
57
+
58
+ // begin writing the confirmation bytes to the child's
59
+ // stdin; use a goroutine since the child hasn't been
60
+ // started yet, and writing sychronously would result
61
+ // in a deadlock
62
+ go func() {
63
+ stdinpipe.Write(expect)
64
+ stdinpipe.Close()
65
+ }()
66
+
67
+ // start the process
68
+ err = cmd.Start()
69
+ if err != nil {
70
+ return 1, fmt.Errorf("starting caddy process: %v", err)
71
+ }
72
+
73
+ // there are two ways we know we're done: either
74
+ // the process will connect to our listener, or
75
+ // it will exit with an error
76
+ success, exit := make(chan struct{}), make(chan error)
77
+
78
+ // in one goroutine, we await the success of the child process
79
+ go func() {
80
+ for {
81
+ conn, err := ln.Accept()
82
+ if err != nil {
83
+ log.Println(err)
84
+ break
85
+ }
86
+ err = handlePingbackConn(conn, expect)
87
+ if err == nil {
88
+ close(success)
89
+ break
90
+ }
91
+ log.Println(err)
92
+ }
93
+ }()
94
+
95
+ // in another goroutine, we await the failure of the child process
96
+ go func() {
97
+ err = cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks
98
+ exit <- err // sending on separate line ensures select won't trigger until after Wait unblocks
99
+ }()
100
+
101
+ // when one of the goroutines unblocks, we're done and can exit
102
+ select {
103
+ case <-success:
104
+ fmt.Println("Successfully started Caddy")
105
+ case err := <-exit:
106
+ return 1, fmt.Errorf("caddy process exited with error: %v", err)
107
+ }
108
+
109
+ return 0, nil
110
+}
111
+
112
+func cmdRun() (int, error) {
113
+ runCmd := flag.NewFlagSet("run", flag.ExitOnError)
114
+ runCmdConfigFlag := runCmd.String("config", "", "Configuration file")
115
+ runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success")
116
+ runCmd.Parse(os.Args[2:])
117
+
118
+ // if a config file was specified for bootstrapping
119
+ // the server instance, load it now
120
+ var config []byte
121
+ if *runCmdConfigFlag != "" {
122
+ var err error
123
+ config, err = ioutil.ReadFile(*runCmdConfigFlag)
124
+ if err != nil {
125
+ return 1, fmt.Errorf("reading config file: %v", err)
126
+ }
127
+ }
128
+
129
+ // start the admin endpoint along with any initial config
130
+ err := caddy.StartAdmin(config)
131
+ if err != nil {
132
+ return 0, fmt.Errorf("starting caddy administration endpoint: %v", err)
133
+ }
134
+ defer caddy.StopAdmin()
135
+
136
+ // if we are to report to another process the successful start
137
+ // of the server, do so now by echoing back contents of stdin
138
+ if *runCmdPingbackFlag != "" {
139
+ confirmationBytes, err := ioutil.ReadAll(os.Stdin)
140
+ if err != nil {
141
+ return 1, fmt.Errorf("reading confirmation bytes from stdin: %v", err)
142
+ }
143
+ conn, err := net.Dial("tcp", *runCmdPingbackFlag)
144
+ if err != nil {
145
+ return 1, fmt.Errorf("dialing confirmation address: %v", err)
146
+ }
147
+ defer conn.Close()
148
+ _, err = conn.Write(confirmationBytes)
149
+ if err != nil {
150
+ return 1, fmt.Errorf("writing confirmation bytes to %s: %v", *runCmdPingbackFlag, err)
151
+ }
152
+ }
153
+
154
+ select {}
155
+}
156
+
157
+func cmdStop() (int, error) {
158
+ processList, err := ps.Processes()
159
+ if err != nil {
160
+ return 1, fmt.Errorf("listing processes: %v", err)
161
+ }
162
+ thisProcName := filepath.Base(os.Args[0])
163
+ var found bool
164
+ for _, p := range processList {
165
+ // the process we're looking for should have the same name but different PID
166
+ if p.Executable() == thisProcName && p.Pid() != os.Getpid() {
167
+ found = true
168
+ fmt.Printf("pid=%d\n", p.Pid())
169
+ fmt.Printf("Graceful stop...")
170
+ if err := gracefullyStopProcess(p.Pid()); err != nil {
171
+ return 1, err
172
+ }
173
+ }
174
+ }
175
+ if !found {
176
+ return 1, fmt.Errorf("Caddy is not running")
177
+ }
178
+ fmt.Println(" success")
179
+ return 0, nil
180
+}
181
+
182
+func cmdVersion() (int, error) {
183
+ goModule := getGoBuildModule()
184
+ if goModule.Sum != "" {
185
+ // a build with a known version will also have a checksum
186
+ fmt.Printf("%s (%s)\n", goModule.Version, goModule.Sum)
187
+ } else {
188
+ fmt.Println(goModule.Version)
189
+ }
190
+ return 0, nil
191
+}
192
+
193
+func cmdListModules() (int, error) {
194
+ for _, m := range caddy.Modules() {
195
+ fmt.Println(m)
196
+ }
197
+ return 0, nil
198
+}
199
+
200
+func cmdEnviron() (int, error) {
201
+ for _, v := range os.Environ() {
202
+ fmt.Println(v)
203
+ }
204
+ return 0, nil
205
+}
cmd/main.go
new
+88
@@ -0,0 +1,88 @@
1
+package caddycmd
2
+
3
+import (
4
+ "bytes"
5
+ "flag"
6
+ "fmt"
7
+ "io"
8
+ "io/ioutil"
9
+ "log"
10
+ "net"
11
+ "os"
12
+ "runtime/debug"
13
+)
14
+
15
+// Main executes the main function of the caddy command.
16
+func Main() {
17
+ if len(os.Args) <= 1 {
18
+ fmt.Println(usageString())
19
+ return
20
+ }
21
+
22
+ subcommand, ok := commands[os.Args[1]]
23
+ if !ok {
24
+ fmt.Printf("%q is not a valid command\n", os.Args[1])
25
+ os.Exit(2)
26
+ }
27
+
28
+ if exitCode, err := subcommand(); err != nil {
29
+ log.Println(err)
30
+ os.Exit(exitCode)
31
+ }
32
+}
33
+
34
+// commandFunc is a function that executes
35
+// a subcommand. It returns an exit code and
36
+// any associated error.
37
+type commandFunc func() (int, error)
38
+
39
+var commands = map[string]commandFunc{
40
+ "start": cmdStart,
41
+ "stop": cmdStop,
42
+ "run": cmdRun,
43
+ "version": cmdVersion,
44
+ "list-modules": cmdListModules,
45
+ "environ": cmdEnviron,
46
+}
47
+
48
+func usageString() string {
49
+ buf := new(bytes.Buffer)
50
+ buf.WriteString("usage: caddy <command> [<args>]")
51
+ flag.CommandLine.SetOutput(buf)
52
+ flag.CommandLine.PrintDefaults()
53
+ return buf.String()
54
+}
55
+
56
+// handlePingbackConn reads from conn and ensures it matches
57
+// the bytes in expect, or returns an error if it doesn't.
58
+func handlePingbackConn(conn net.Conn, expect []byte) error {
59
+ defer conn.Close()
60
+ confirmationBytes, err := ioutil.ReadAll(io.LimitReader(conn, 32))
61
+ if err != nil {
62
+ return err
63
+ }
64
+ if !bytes.Equal(confirmationBytes, expect) {
65
+ return fmt.Errorf("wrong confirmation: %x", confirmationBytes)
66
+ }
67
+ return nil
68
+}
69
+
70
+// getGoBuildModule returns the build info of Caddy
71
+// from debug.BuildInfo (requires Go modules). If
72
+// no version information is available, a non-nil
73
+// value will still be returned, but with an
74
+// unknown version.
75
+func getGoBuildModule() *debug.Module {
76
+ bi, ok := debug.ReadBuildInfo()
77
+ if ok {
78
+ // The recommended way to build Caddy involves
79
+ // creating a separate main module, which
80
+ // TODO: track related Go issue: https://github.com/golang/go/issues/29228
81
+ for _, mod := range bi.Deps {
82
+ if mod.Path == "github.com/mholt/caddy" {
83
+ return mod
84
+ }
85
+ }
86
+ }
87
+ return &debug.Module{Version: "unknown"}
88
+}
cmd/proc_posix.go
new
+16
@@ -0,0 +1,16 @@
1
+// +build !windows
2
+
3
+package caddycmd
4
+
5
+import (
6
+ "fmt"
7
+ "syscall"
8
+)
9
+
10
+func gracefullyStopProcess(pid int) error {
11
+ err := syscall.Kill(pid, syscall.SIGINT)
12
+ if err != nil {
13
+ return fmt.Errorf("kill: %v", err)
14
+ }
15
+ return nil
16
+}
cmd/proc_windows.go
new
+15
@@ -0,0 +1,15 @@
1
+package caddycmd
2
+
3
+import (
4
+ "fmt"
5
+ "os/exec"
6
+ "strconv"
7
+)
8
+
9
+func gracefullyStopProcess(pid int) error {
10
+ cmd := exec.Command("taskkill", "/pid", strconv.Itoa(pid))
11
+ if err := cmd.Run(); err != nil {
12
+ return fmt.Errorf("taskkill: %v", err)
13
+ }
14
+ return nil
15
+}
cmd/run.go
deleted
-26
@@ -1,26 +0,0 @@
1
-package caddycmd
2
-
3
-import (
4
- "flag"
5
- "log"
6
-
7
- "github.com/caddyserver/caddy"
8
-)
9
-
10
-// Main executes the main function of the caddy command.
11
-func Main() {
12
- flag.Parse()
13
-
14
- err := caddy.StartAdmin(*listenAddr)
15
- if err != nil {
16
- log.Fatal(err)
17
- }
18
- defer caddy.StopAdmin()
19
-
20
- log.Println("Caddy 2 admin endpoint listening on", *listenAddr)
21
-
22
- select {}
23
-}
24
-
25
-// TODO: for dev only
26
-var listenAddr = flag.String("listen", ":1234", "The admin endpoint listener address")