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: Refactor subcommands, add help, make them pluggable
* cli: Change command structure, add help subcommand (#328) * cli: improve subcommand structure - make help command as normal subcommand - add flag usage message for each command * cmd: Refactor subcommands and command line help; make commands pluggable
aca committed
Oct 1, 2019 at 12:23 UTC
0006df60264e7fdc55fc1a0962109e8198fa9d71
3 files changed
+671
-347
cmd/commandfuncs.go
new
+391
@@ -0,0 +1,391 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddycmd
16
+
17
+import (
18
+ "bytes"
19
+ "crypto/rand"
20
+ "encoding/json"
21
+ "fmt"
22
+ "io"
23
+ "io/ioutil"
24
+ "log"
25
+ "net"
26
+ "net/http"
27
+ "os"
28
+ "os/exec"
29
+ "strings"
30
+
31
+ "github.com/caddyserver/caddy/v2"
32
+ "github.com/caddyserver/caddy/v2/caddyconfig"
33
+ "github.com/keybase/go-ps"
34
+ "github.com/mholt/certmagic"
35
+)
36
+
37
+func cmdStart(fl Flags) (int, error) {
38
+ startCmdConfigFlag := fl.String("config")
39
+ startCmdConfigAdapterFlag := fl.String("config-adapter")
40
+
41
+ // open a listener to which the child process will connect when
42
+ // it is ready to confirm that it has successfully started
43
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
44
+ if err != nil {
45
+ return caddy.ExitCodeFailedStartup,
46
+ fmt.Errorf("opening listener for success confirmation: %v", err)
47
+ }
48
+ defer ln.Close()
49
+
50
+ // craft the command with a pingback address and with a
51
+ // pipe for its stdin, so we can tell it our confirmation
52
+ // code that we expect so that some random port scan at
53
+ // the most unfortunate time won't fool us into thinking
54
+ // the child succeeded (i.e. the alternative is to just
55
+ // wait for any connection on our listener, but better to
56
+ // ensure it's the process we're expecting - we can be
57
+ // sure by giving it some random bytes and having it echo
58
+ // them back to us)
59
+ cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
60
+ if startCmdConfigFlag != "" {
61
+ cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag)
62
+ }
63
+ if startCmdConfigAdapterFlag != "" {
64
+ cmd.Args = append(cmd.Args, "--config-adapter", startCmdConfigAdapterFlag)
65
+ }
66
+ stdinpipe, err := cmd.StdinPipe()
67
+ if err != nil {
68
+ return caddy.ExitCodeFailedStartup,
69
+ fmt.Errorf("creating stdin pipe: %v", err)
70
+ }
71
+ cmd.Stdout = os.Stdout
72
+ cmd.Stderr = os.Stderr
73
+
74
+ // generate the random bytes we'll send to the child process
75
+ expect := make([]byte, 32)
76
+ _, err = rand.Read(expect)
77
+ if err != nil {
78
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err)
79
+ }
80
+
81
+ // begin writing the confirmation bytes to the child's
82
+ // stdin; use a goroutine since the child hasn't been
83
+ // started yet, and writing sychronously would result
84
+ // in a deadlock
85
+ go func() {
86
+ stdinpipe.Write(expect)
87
+ stdinpipe.Close()
88
+ }()
89
+
90
+ // start the process
91
+ err = cmd.Start()
92
+ if err != nil {
93
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
94
+ }
95
+
96
+ // there are two ways we know we're done: either
97
+ // the process will connect to our listener, or
98
+ // it will exit with an error
99
+ success, exit := make(chan struct{}), make(chan error)
100
+
101
+ // in one goroutine, we await the success of the child process
102
+ go func() {
103
+ for {
104
+ conn, err := ln.Accept()
105
+ if err != nil {
106
+ if !strings.Contains(err.Error(), "use of closed network connection") {
107
+ log.Println(err)
108
+ }
109
+ break
110
+ }
111
+ err = handlePingbackConn(conn, expect)
112
+ if err == nil {
113
+ close(success)
114
+ break
115
+ }
116
+ log.Println(err)
117
+ }
118
+ }()
119
+
120
+ // in another goroutine, we await the failure of the child process
121
+ go func() {
122
+ err := cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks
123
+ exit <- err // sending on separate line ensures select won't trigger until after Wait unblocks
124
+ }()
125
+
126
+ // when one of the goroutines unblocks, we're done and can exit
127
+ select {
128
+ case <-success:
129
+ fmt.Printf("Successfully started Caddy (pid=%d)\n", cmd.Process.Pid)
130
+ case err := <-exit:
131
+ return caddy.ExitCodeFailedStartup,
132
+ fmt.Errorf("caddy process exited with error: %v", err)
133
+ }
134
+
135
+ return caddy.ExitCodeSuccess, nil
136
+}
137
+
138
+func cmdRun(fl Flags) (int, error) {
139
+ runCmdConfigFlag := fl.String("config")
140
+ runCmdConfigAdapterFlag := fl.String("config-adapter")
141
+ runCmdPrintEnvFlag := fl.Bool("print-env")
142
+ runCmdPingbackFlag := fl.String("pingback")
143
+
144
+ // if we are supposed to print the environment, do that first
145
+ if runCmdPrintEnvFlag {
146
+ printEnvironment()
147
+ }
148
+
149
+ // get the config in caddy's native format
150
+ config, err := loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
151
+ if err != nil {
152
+ return caddy.ExitCodeFailedStartup, err
153
+ }
154
+
155
+ // set a fitting User-Agent for ACME requests
156
+ goModule := caddy.GoModule()
157
+ cleanModVersion := strings.TrimPrefix(goModule.Version, "v")
158
+ certmagic.UserAgent = "Caddy/" + cleanModVersion
159
+
160
+ // start the admin endpoint along with any initial config
161
+ err = caddy.StartAdmin(config)
162
+ if err != nil {
163
+ return caddy.ExitCodeFailedStartup,
164
+ fmt.Errorf("starting caddy administration endpoint: %v", err)
165
+ }
166
+ defer caddy.StopAdmin()
167
+
168
+ // if we are to report to another process the successful start
169
+ // of the server, do so now by echoing back contents of stdin
170
+ if runCmdPingbackFlag != "" {
171
+ confirmationBytes, err := ioutil.ReadAll(os.Stdin)
172
+ if err != nil {
173
+ return caddy.ExitCodeFailedStartup,
174
+ fmt.Errorf("reading confirmation bytes from stdin: %v", err)
175
+ }
176
+ conn, err := net.Dial("tcp", runCmdPingbackFlag)
177
+ if err != nil {
178
+ return caddy.ExitCodeFailedStartup,
179
+ fmt.Errorf("dialing confirmation address: %v", err)
180
+ }
181
+ defer conn.Close()
182
+ _, err = conn.Write(confirmationBytes)
183
+ if err != nil {
184
+ return caddy.ExitCodeFailedStartup,
185
+ fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err)
186
+ }
187
+ }
188
+
189
+ select {}
190
+}
191
+
192
+func cmdStop(_ Flags) (int, error) {
193
+ processList, err := ps.Processes()
194
+ if err != nil {
195
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err)
196
+ }
197
+ thisProcName := getProcessName()
198
+ var found bool
199
+ for _, p := range processList {
200
+ // the process we're looking for should have the same name but different PID
201
+ if p.Executable() == thisProcName && p.Pid() != os.Getpid() {
202
+ found = true
203
+ fmt.Printf("pid=%d\n", p.Pid())
204
+
205
+ if err := gracefullyStopProcess(p.Pid()); err != nil {
206
+ return caddy.ExitCodeFailedStartup, err
207
+ }
208
+ }
209
+ }
210
+ if !found {
211
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running")
212
+ }
213
+ fmt.Println(" success")
214
+ return caddy.ExitCodeSuccess, nil
215
+}
216
+
217
+func cmdReload(fl Flags) (int, error) {
218
+ reloadCmdConfigFlag := fl.String("config")
219
+ reloadCmdConfigAdapterFlag := fl.String("config-adapter")
220
+ reloadCmdAddrFlag := fl.String("address")
221
+
222
+ // a configuration is required
223
+ if reloadCmdConfigFlag == "" {
224
+ return caddy.ExitCodeFailedStartup,
225
+ fmt.Errorf("no configuration to load (use --config)")
226
+ }
227
+
228
+ // get the config in caddy's native format
229
+ config, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
230
+ if err != nil {
231
+ return caddy.ExitCodeFailedStartup, err
232
+ }
233
+
234
+ // get the address of the admin listener and craft endpoint URL
235
+ adminAddr := reloadCmdAddrFlag
236
+ if adminAddr == "" {
237
+ var tmpStruct struct {
238
+ Admin caddy.AdminConfig `json:"admin"`
239
+ }
240
+ err = json.Unmarshal(config, &tmpStruct)
241
+ if err != nil {
242
+ return caddy.ExitCodeFailedStartup,
243
+ fmt.Errorf("unmarshaling admin listener address from config: %v", err)
244
+ }
245
+ adminAddr = tmpStruct.Admin.Listen
246
+ }
247
+ if adminAddr == "" {
248
+ adminAddr = caddy.DefaultAdminListen
249
+ }
250
+ adminEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
251
+
252
+ // send the configuration to the instance
253
+ resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config))
254
+ if err != nil {
255
+ return caddy.ExitCodeFailedStartup,
256
+ fmt.Errorf("sending configuration to instance: %v", err)
257
+ }
258
+ defer resp.Body.Close()
259
+
260
+ // if it didn't work, let the user know
261
+ if resp.StatusCode >= 400 {
262
+ respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10))
263
+ if err != nil {
264
+ return caddy.ExitCodeFailedStartup,
265
+ fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
266
+ }
267
+ return caddy.ExitCodeFailedStartup,
268
+ fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
269
+ }
270
+
271
+ return caddy.ExitCodeSuccess, nil
272
+}
273
+
274
+func cmdVersion(_ Flags) (int, error) {
275
+ goModule := caddy.GoModule()
276
+ if goModule.Sum != "" {
277
+ // a build with a known version will also have a checksum
278
+ fmt.Printf("%s %s\n", goModule.Version, goModule.Sum)
279
+ } else {
280
+ fmt.Println(goModule.Version)
281
+ }
282
+ return caddy.ExitCodeSuccess, nil
283
+}
284
+
285
+func cmdListModules(_ Flags) (int, error) {
286
+ for _, m := range caddy.Modules() {
287
+ fmt.Println(m)
288
+ }
289
+ return caddy.ExitCodeSuccess, nil
290
+}
291
+
292
+func cmdEnviron(_ Flags) (int, error) {
293
+ printEnvironment()
294
+ return caddy.ExitCodeSuccess, nil
295
+}
296
+
297
+func cmdAdaptConfig(fl Flags) (int, error) {
298
+ adaptCmdAdapterFlag := fl.String("adapter")
299
+ adaptCmdInputFlag := fl.String("input")
300
+ adaptCmdPrettyFlag := fl.Bool("pretty")
301
+
302
+ if adaptCmdAdapterFlag == "" || adaptCmdInputFlag == "" {
303
+ return caddy.ExitCodeFailedStartup,
304
+ fmt.Errorf("usage: caddy adapt-config --adapter <name> --input <file>")
305
+ }
306
+
307
+ cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag)
308
+ if cfgAdapter == nil {
309
+ return caddy.ExitCodeFailedStartup,
310
+ fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag)
311
+ }
312
+
313
+ input, err := ioutil.ReadFile(adaptCmdInputFlag)
314
+ if err != nil {
315
+ return caddy.ExitCodeFailedStartup,
316
+ fmt.Errorf("reading input file: %v", err)
317
+ }
318
+
319
+ opts := make(map[string]interface{})
320
+ if adaptCmdPrettyFlag {
321
+ opts["pretty"] = "true"
322
+ }
323
+
324
+ adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
325
+ if err != nil {
326
+ return caddy.ExitCodeFailedStartup, err
327
+ }
328
+
329
+ // print warnings to stderr
330
+ for _, warn := range warnings {
331
+ msg := warn.Message
332
+ if warn.Directive != "" {
333
+ msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
334
+ }
335
+ log.Printf("[WARNING][%s] %s:%d: %s", adaptCmdAdapterFlag, warn.File, warn.Line, msg)
336
+ }
337
+
338
+ // print result to stdout
339
+ fmt.Println(string(adaptedConfig))
340
+
341
+ return caddy.ExitCodeSuccess, nil
342
+}
343
+
344
+func cmdHelp(fl Flags) (int, error) {
345
+ const fullDocs = `Full documentation is available at:
346
+https://github.com/caddyserver/caddy/wiki/v2:-Documentation`
347
+
348
+ args := fl.Args()
349
+ if len(args) == 0 {
350
+ s := `Caddy is an extensible server platform.
351
+
352
+usage:
353
+ caddy <command> [<args...>]
354
+
355
+commands:
356
+`
357
+ for _, cmd := range commands {
358
+ s += fmt.Sprintf(" %-15s %s\n", cmd.Name, cmd.Short)
359
+ }
360
+
361
+ s += "\nUse 'caddy help <command>' for more information about a command.\n"
362
+ s += "\n" + fullDocs + "\n"
363
+
364
+ fmt.Print(s)
365
+
366
+ return caddy.ExitCodeSuccess, nil
367
+ } else if len(args) > 1 {
368
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("can only give help with one command")
369
+ }
370
+
371
+ subcommand, ok := commands[args[0]]
372
+ if !ok {
373
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("unknown command: %s", args[0])
374
+ }
375
+
376
+ result := fmt.Sprintf("%s\n\nusage:\n caddy %s %s\n",
377
+ strings.TrimSpace(subcommand.Long),
378
+ subcommand.Name,
379
+ strings.TrimSpace(subcommand.Usage),
380
+ )
381
+
382
+ if help := flagHelp(subcommand.Flags); help != "" {
383
+ result += fmt.Sprintf("\nflags:\n%s", help)
384
+ }
385
+
386
+ result += "\n" + fullDocs + "\n"
387
+
388
+ fmt.Print(result)
389
+
390
+ return caddy.ExitCodeSuccess, nil
391
+}
cmd/commands.go
+179
-318
@@ -15,343 +15,204 @@
15
package caddycmd
16
17
import (
18
- "bytes"
19
- "crypto/rand"
20
- "encoding/json"
18
"flag"
22
- "fmt"
23
- "io"
24
- "io/ioutil"
25
- "log"
26
- "net"
27
- "net/http"
28
- "os"
29
- "os/exec"
30
-
31
- "strings"
32
-
33
- "github.com/caddyserver/caddy/v2"
34
- "github.com/caddyserver/caddy/v2/caddyconfig"
35
- "github.com/keybase/go-ps"
36
- "github.com/mholt/certmagic"
19
+ "regexp"
20
)
21
39
-func cmdStart() (int, error) {
40
- startCmd := flag.NewFlagSet("start", flag.ExitOnError)
41
- startCmdConfigFlag := startCmd.String("config", "", "Configuration file")
42
- startCmdConfigAdapterFlag := startCmd.String("config-adapter", "", "Name of config adapter to apply")
43
- startCmd.Parse(os.Args[2:])
22
+// Command represents a subcommand. All fields
23
+// are required to be set except for Flags if
24
+// there are no flags and Usage if there are
25
+// no flags or arguments.
26
+type Command struct {
27
+ Name string
28
45
- // open a listener to which the child process will connect when
46
- // it is ready to confirm that it has successfully started
47
- ln, err := net.Listen("tcp", "127.0.0.1:0")
48
- if err != nil {
49
- return caddy.ExitCodeFailedStartup,
50
- fmt.Errorf("opening listener for success confirmation: %v", err)
51
- }
52
- defer ln.Close()
29
+ // Run is a function that executes a subcommand.
30
+ // It returns an exit code and any associated error.
31
+ // Takes non-flag commandline arguments as args.
32
+ // Flag must be parsed before Run is executed.
33
+ Func CommandFunc
34
54
- // craft the command with a pingback address and with a
55
- // pipe for its stdin, so we can tell it our confirmation
56
- // code that we expect so that some random port scan at
57
- // the most unfortunate time won't fool us into thinking
58
- // the child succeeded (i.e. the alternative is to just
59
- // wait for any connection on our listener, but better to
60
- // ensure it's the process we're expecting - we can be
61
- // sure by giving it some random bytes and having it echo
62
- // them back to us)
63
- cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
64
- if *startCmdConfigFlag != "" {
65
- cmd.Args = append(cmd.Args, "--config", *startCmdConfigFlag)
66
- }
67
- if *startCmdConfigAdapterFlag != "" {
68
- cmd.Args = append(cmd.Args, "--config-adapter", *startCmdConfigAdapterFlag)
69
- }
70
- stdinpipe, err := cmd.StdinPipe()
71
- if err != nil {
72
- return caddy.ExitCodeFailedStartup,
73
- fmt.Errorf("creating stdin pipe: %v", err)
74
- }
75
- cmd.Stdout = os.Stdout
76
- cmd.Stderr = os.Stderr
35
+ // Usage is the one-line message explaining args, flags.
36
+ Usage string
37
78
- // generate the random bytes we'll send to the child process
79
- expect := make([]byte, 32)
80
- _, err = rand.Read(expect)
81
- if err != nil {
82
- return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err)
83
- }
38
+ // Short is the short description for command.
39
+ Short string
40
85
- // begin writing the confirmation bytes to the child's
86
- // stdin; use a goroutine since the child hasn't been
87
- // started yet, and writing sychronously would result
88
- // in a deadlock
89
- go func() {
90
- stdinpipe.Write(expect)
91
- stdinpipe.Close()
92
- }()
41
+ // Long is the message for 'caddy help <command>'
42
+ Long string
43
94
- // start the process
95
- err = cmd.Start()
96
- if err != nil {
97
- return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
98
- }
99
-
100
- // there are two ways we know we're done: either
101
- // the process will connect to our listener, or
102
- // it will exit with an error
103
- success, exit := make(chan struct{}), make(chan error)
104
-
105
- // in one goroutine, we await the success of the child process
106
- go func() {
107
- for {
108
- conn, err := ln.Accept()
109
- if err != nil {
110
- if !strings.Contains(err.Error(), "use of closed network connection") {
111
- log.Println(err)
112
- }
113
- break
114
- }
115
- err = handlePingbackConn(conn, expect)
116
- if err == nil {
117
- close(success)
118
- break
119
- }
120
- log.Println(err)
121
- }
122
- }()
123
-
124
- // in another goroutine, we await the failure of the child process
125
- go func() {
126
- err := cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks
127
- exit <- err // sending on separate line ensures select won't trigger until after Wait unblocks
128
- }()
129
-
130
- // when one of the goroutines unblocks, we're done and can exit
131
- select {
132
- case <-success:
133
- fmt.Printf("Successfully started Caddy (pid=%d)\n", cmd.Process.Pid)
134
- case err := <-exit:
135
- return caddy.ExitCodeFailedStartup,
136
- fmt.Errorf("caddy process exited with error: %v", err)
137
- }
138
-
139
- return caddy.ExitCodeSuccess, nil
44
+ // Flags is flagset for command.
45
+ Flags *flag.FlagSet
46
}
47
142
-func cmdRun() (int, error) {
143
- runCmd := flag.NewFlagSet("run", flag.ExitOnError)
144
- runCmdConfigFlag := runCmd.String("config", "", "Configuration file")
145
- runCmdConfigAdapterFlag := runCmd.String("config-adapter", "", "Name of config adapter to apply")
146
- runCmdPrintEnvFlag := runCmd.Bool("print-env", false, "Print environment")
147
- runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success")
148
- runCmd.Parse(os.Args[2:])
149
-
150
- // if we are supposed to print the environment, do that first
151
- if *runCmdPrintEnvFlag {
152
- exitCode, err := cmdEnviron()
153
- if err != nil {
154
- return exitCode, err
155
- }
156
- }
157
-
158
- // get the config in caddy's native format
159
- config, err := loadConfig(*runCmdConfigFlag, *runCmdConfigAdapterFlag)
160
- if err != nil {
161
- return caddy.ExitCodeFailedStartup, err
162
- }
163
-
164
- // set a fitting User-Agent for ACME requests
165
- goModule := caddy.GoModule()
166
- cleanModVersion := strings.TrimPrefix(goModule.Version, "v")
167
- certmagic.UserAgent = "Caddy/" + cleanModVersion
168
-
169
- // start the admin endpoint along with any initial config
170
- err = caddy.StartAdmin(config)
171
- if err != nil {
172
- return caddy.ExitCodeFailedStartup,
173
- fmt.Errorf("starting caddy administration endpoint: %v", err)
174
- }
175
- defer caddy.StopAdmin()
176
-
177
- // if we are to report to another process the successful start
178
- // of the server, do so now by echoing back contents of stdin
179
- if *runCmdPingbackFlag != "" {
180
- confirmationBytes, err := ioutil.ReadAll(os.Stdin)
181
- if err != nil {
182
- return caddy.ExitCodeFailedStartup,
183
- fmt.Errorf("reading confirmation bytes from stdin: %v", err)
184
- }
185
- conn, err := net.Dial("tcp", *runCmdPingbackFlag)
186
- if err != nil {
187
- return caddy.ExitCodeFailedStartup,
188
- fmt.Errorf("dialing confirmation address: %v", err)
189
- }
190
- defer conn.Close()
191
- _, err = conn.Write(confirmationBytes)
192
- if err != nil {
193
- return caddy.ExitCodeFailedStartup,
194
- fmt.Errorf("writing confirmation bytes to %s: %v", *runCmdPingbackFlag, err)
195
- }
196
- }
197
-
198
- select {}
48
+// CommandFunc is a command's function. It runs the
49
+// command and returns the proper exit code along with
50
+// any error that occurred.
51
+type CommandFunc func(Flags) (int, error)
52
+
53
+var commands = map[string]Command{
54
+ "start": {
55
+ Name: "start",
56
+ Func: cmdStart,
57
+ Usage: "[--config <path>] [--config-adapter <name>]",
58
+ Short: "Starts the Caddy process and returns after server has started.",
59
+ Long: `
60
+Starts the Caddy process, optionally bootstrapped with an initial
61
+config file. Blocks until server is successfully running (or fails to run),
62
+then returns. On Windows, the child process will remain attached to the
63
+terminal, so closing the window will forcefully stop Caddy. See run for more
64
+details.`,
65
+ Flags: func() *flag.FlagSet {
66
+ fs := flag.NewFlagSet("start", flag.ExitOnError)
67
+ fs.String("config", "", "Configuration file")
68
+ fs.String("config-adapter", "", "Name of config adapter to apply")
69
+ return fs
70
+ }(),
71
+ },
72
+
73
+ "run": {
74
+ Name: "run",
75
+ Func: cmdRun,
76
+ Usage: "[--config <path>] [--config-adapter <name>] [--print-env]",
77
+ Short: `Starts the Caddy process and blocks indefinitely.`,
78
+ Long: `
79
+Same as start, but blocks indefinitely; i.e. runs Caddy in "daemon" mode. On
80
+Windows, this is recommended over caddy start when running Caddy manually since
81
+it will be more obvious that Caddy is still running and bound to the terminal
82
+window.
83
+
84
+If a config file is specified, it will be applied immediately after the process
85
+is running. If the config file is not in Caddy's native JSON format, you can
86
+specify an adapter with --config-adapter to adapt the given config file to
87
+Caddy's native format. The config adapter must be a registered module. Any
88
+warnings will be printed to the log, but beware that any adaptation without
89
+errors will immediately be used. If you want to review the results of the
90
+adaptation first, use adapt-config.
91
+
92
+As a special case, if the current working directory has a file called
93
+"Caddyfile" and the caddyfile config adapter is plugged in (default), then that
94
+file will be loaded and used to configure Caddy, even without any command line
95
+flags.
96
+
97
+If --print-env is specified, the environment as seen by the Caddy process will
98
+be printed before starting. This is the same as the environ command but does
99
+not quit after printing.`,
100
+ Flags: func() *flag.FlagSet {
101
+ fs := flag.NewFlagSet("run", flag.ExitOnError)
102
+ fs.String("config", "", "Configuration file")
103
+ fs.String("config-adapter", "", "Name of config adapter to apply")
104
+ fs.Bool("print-env", false, "Print environment")
105
+ fs.String("pingback", "", "Echo confirmation bytes to this address on success")
106
+ return fs
107
+ }(),
108
+ },
109
+
110
+ "stop": {
111
+ Name: "stop",
112
+ Func: cmdStop,
113
+ Short: "Gracefully stops the running Caddy process",
114
+ Long: `Gracefully stops the running Caddy process. (Note: this will stop any process
115
+named the same as the executable.) On Windows, this stop is forceful and Caddy
116
+will not have an opportunity to clean up any active locks; for a graceful
117
+shutdown on Windows, use Ctrl+C or the /stop endpoint.`,
118
+ },
119
+
120
+ "reload": {
121
+ Name: "reload",
122
+ Func: cmdReload,
123
+ Usage: "--config <path> [--config-adapter <name>] [--address <interface>]",
124
+ Short: "Gives the running Caddy instance a new configuration",
125
+ Long: `Gives the running Caddy instance a new configuration. This has the same effect
126
+as POSTing a document to the /load endpoint, but is convenient for simple
127
+workflows revolving around config files. Since the admin endpoint is
128
+configurable, the endpoint configuration is loaded from the --address flag if
129
+specified; otherwise it is loaded from the given config file; otherwise the
130
+default is assumed.`,
131
+ Flags: func() *flag.FlagSet {
132
+ fs := flag.NewFlagSet("load", flag.ExitOnError)
133
+ fs.String("config", "", "Configuration file")
134
+ fs.String("config-adapter", "", "Name of config adapter to apply")
135
+ fs.String("address", "", "Address of the administration listener, if different from config")
136
+ return fs
137
+ }(),
138
+ },
139
+
140
+ "version": {
141
+ Name: "version",
142
+ Func: cmdVersion,
143
+ Short: "Prints the version.",
144
+ Long: `Prints the version.`,
145
+ },
146
+
147
+ "list-modules": {
148
+ Name: "list-modules",
149
+ Func: cmdListModules,
150
+ Short: "List installed Caddy modules.",
151
+ Long: `List installed Caddy modules.`,
152
+ },
153
+
154
+ "environ": {
155
+ Name: "environ",
156
+ Func: cmdEnviron,
157
+ Short: "Prints the environment as seen by Caddy.",
158
+ Long: `Prints the environment as seen by Caddy.`,
159
+ },
160
+
161
+ "adapt-config": {
162
+ Name: "adapt-config",
163
+ Func: cmdAdaptConfig,
164
+ Usage: "--input <path> --adapter <name> [--pretty]",
165
+ Short: "Adapts a configuration to Caddy's native JSON config structure",
166
+ Long: `
167
+Adapts a configuration to Caddy's native JSON config structure and writes the
168
+output to stdout, along with any warnings to stderr. If --pretty is specified,
169
+the output will be formatted with indentation for human readability.`,
170
+ },
171
}
172
201
-func cmdStop() (int, error) {
202
- processList, err := ps.Processes()
203
- if err != nil {
204
- return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err)
205
- }
206
- thisProcName := getProcessName()
207
- var found bool
208
- for _, p := range processList {
209
- // the process we're looking for should have the same name but different PID
210
- if p.Executable() == thisProcName && p.Pid() != os.Getpid() {
211
- found = true
212
- fmt.Printf("pid=%d\n", p.Pid())
213
-
214
- if err := gracefullyStopProcess(p.Pid()); err != nil {
215
- return caddy.ExitCodeFailedStartup, err
216
- }
217
- }
218
- }
219
- if !found {
220
- return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running")
173
+func init() {
174
+ // the help command is special in that its func
175
+ // refers to the commands map; thus, defining it
176
+ // inline with the commands map's initialization
177
+ // yields a compile-time error, so we have to
178
+ // define this command separately
179
+ commands["help"] = Command{
180
+ Name: "help",
181
+ Func: cmdHelp,
182
+ Usage: "<command>",
183
+ Short: "Shows help for a Caddy subcommand.",
184
}
222
- fmt.Println(" success")
223
- return caddy.ExitCodeSuccess, nil
185
}
186
226
-func cmdReload() (int, error) {
227
- reloadCmd := flag.NewFlagSet("load", flag.ExitOnError)
228
- reloadCmdConfigFlag := reloadCmd.String("config", "", "Configuration file")
229
- reloadCmdConfigAdapterFlag := reloadCmd.String("config-adapter", "", "Name of config adapter to apply")
230
- reloadCmdAddrFlag := reloadCmd.String("address", "", "Address of the administration listener, if different from config")
231
- reloadCmd.Parse(os.Args[2:])
232
-
233
- // a configuration is required
234
- if *reloadCmdConfigFlag == "" {
235
- return caddy.ExitCodeFailedStartup,
236
- fmt.Errorf("no configuration to load (use --config)")
237
- }
238
-
239
- // get the config in caddy's native format
240
- config, err := loadConfig(*reloadCmdConfigFlag, *reloadCmdConfigAdapterFlag)
241
- if err != nil {
242
- return caddy.ExitCodeFailedStartup, err
243
- }
244
-
245
- // get the address of the admin listener and craft endpoint URL
246
- adminAddr := *reloadCmdAddrFlag
247
- if adminAddr == "" {
248
- var tmpStruct struct {
249
- Admin caddy.AdminConfig `json:"admin"`
250
- }
251
- err = json.Unmarshal(config, &tmpStruct)
252
- if err != nil {
253
- return caddy.ExitCodeFailedStartup,
254
- fmt.Errorf("unmarshaling admin listener address from config: %v", err)
255
- }
256
- adminAddr = tmpStruct.Admin.Listen
187
+// RegisterCommand registers the command cmd.
188
+// cmd.Name must be unique and conform to the
189
+// following format:
190
+//
191
+// - lowercase
192
+// - alphanumeric and hyphen characters only
193
+// - cannot start or end with a hyphen
194
+// - hyphen cannot be adjacent to another hyphen
195
+//
196
+// This function panics if the name is already registered,
197
+// if the name does not meet the described format, or if
198
+// any of the fields are missing from cmd.
199
+func RegisterCommand(cmd Command) {
200
+ if cmd.Name == "" {
201
+ panic("command name is required")
202
}
258
- if adminAddr == "" {
259
- adminAddr = caddy.DefaultAdminListen
203
+ if cmd.Func == nil {
204
+ panic("command function missing")
205
}
261
- adminEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
262
-
263
- // send the configuration to the instance
264
- resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config))
265
- if err != nil {
266
- return caddy.ExitCodeFailedStartup,
267
- fmt.Errorf("sending configuration to instance: %v", err)
206
+ if cmd.Short == "" {
207
+ panic("command short string is required")
208
}
269
- defer resp.Body.Close()
270
-
271
- // if it didn't work, let the user know
272
- if resp.StatusCode >= 400 {
273
- respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10))
274
- if err != nil {
275
- return caddy.ExitCodeFailedStartup,
276
- fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
277
- }
278
- return caddy.ExitCodeFailedStartup,
279
- fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
209
+ if _, exists := commands[cmd.Name]; exists {
210
+ panic("command already registered: " + cmd.Name)
211
}
281
-
282
- return caddy.ExitCodeSuccess, nil
283
-}
284
-
285
-func cmdVersion() (int, error) {
286
- goModule := caddy.GoModule()
287
- if goModule.Sum != "" {
288
- // a build with a known version will also have a checksum
289
- fmt.Printf("%s %s\n", goModule.Version, goModule.Sum)
290
- } else {
291
- fmt.Println(goModule.Version)
292
- }
293
- return caddy.ExitCodeSuccess, nil
294
-}
295
-
296
-func cmdListModules() (int, error) {
297
- for _, m := range caddy.Modules() {
298
- fmt.Println(m)
299
- }
300
- return caddy.ExitCodeSuccess, nil
301
-}
302
-
303
-func cmdEnviron() (int, error) {
304
- for _, v := range os.Environ() {
305
- fmt.Println(v)
212
+ if !commandNameRegex.MatchString(cmd.Name) {
213
+ panic("invalid command name")
214
}
307
- return caddy.ExitCodeSuccess, nil
215
+ commands[cmd.Name] = cmd
216
}
217
310
-func cmdAdaptConfig() (int, error) {
311
- adaptCmd := flag.NewFlagSet("adapt", flag.ExitOnError)
312
- adaptCmdAdapterFlag := adaptCmd.String("adapter", "", "Name of config adapter")
313
- adaptCmdInputFlag := adaptCmd.String("input", "", "Configuration file to adapt")
314
- adaptCmdPrettyFlag := adaptCmd.Bool("pretty", false, "Format the output for human readability")
315
- adaptCmd.Parse(os.Args[2:])
316
-
317
- if *adaptCmdAdapterFlag == "" || *adaptCmdInputFlag == "" {
318
- return caddy.ExitCodeFailedStartup,
319
- fmt.Errorf("usage: caddy adapt-config --adapter <name> --input <file>")
320
- }
321
-
322
- cfgAdapter := caddyconfig.GetAdapter(*adaptCmdAdapterFlag)
323
- if cfgAdapter == nil {
324
- return caddy.ExitCodeFailedStartup,
325
- fmt.Errorf("unrecognized config adapter: %s", *adaptCmdAdapterFlag)
326
- }
327
-
328
- input, err := ioutil.ReadFile(*adaptCmdInputFlag)
329
- if err != nil {
330
- return caddy.ExitCodeFailedStartup,
331
- fmt.Errorf("reading input file: %v", err)
332
- }
333
-
334
- opts := make(map[string]interface{})
335
- if *adaptCmdPrettyFlag {
336
- opts["pretty"] = "true"
337
- }
338
-
339
- adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
340
- if err != nil {
341
- return caddy.ExitCodeFailedStartup, err
342
- }
343
-
344
- // print warnings to stderr
345
- for _, warn := range warnings {
346
- msg := warn.Message
347
- if warn.Directive != "" {
348
- msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
349
- }
350
- log.Printf("[WARNING][%s] %s:%d: %s", *adaptCmdAdapterFlag, warn.File, warn.Line, msg)
351
- }
352
-
353
- // print result to stdout
354
- fmt.Println(string(adaptedConfig))
355
-
356
- return caddy.ExitCodeSuccess, nil
357
-}
218
+var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
cmd/main.go
+101
-29
@@ -23,6 +23,9 @@ import (
23
"log"
24
"net"
25
"os"
26
+ "strconv"
27
+ "strings"
28
+ "time"
29
30
"github.com/caddyserver/caddy/v2"
31
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -33,45 +36,43 @@ import (
36
func Main() {
37
caddy.TrapSignals()
38
36
- if len(os.Args) < 2 {
37
- fmt.Println(usageString())
38
- return
39
+ switch len(os.Args) {
40
+ case 0:
41
+ log.Printf("[FATAL] no arguments provided by OS; args[0] must be command")
42
+ os.Exit(caddy.ExitCodeFailedStartup)
43
+ case 1:
44
+ os.Args = append(os.Args, "help")
45
}
46
41
- subcommand, ok := commands[os.Args[1]]
47
+ subcommandName := os.Args[1]
48
+ subcommand, ok := commands[subcommandName]
49
if !ok {
43
- fmt.Printf("%q is not a valid command\n", os.Args[1])
50
+ if strings.HasPrefix(os.Args[1], "-") {
51
+ // user probably forgot to type the subcommand
52
+ log.Println("[ERROR] first argument must be a subcommand; see 'caddy help'")
53
+ } else {
54
+ log.Printf("[ERROR] '%s' is not a recognized subcommand; see 'caddy help'", os.Args[1])
55
+ }
56
os.Exit(caddy.ExitCodeFailedStartup)
57
}
58
47
- if exitCode, err := subcommand(); err != nil {
59
+ fs := subcommand.Flags
60
+ if fs == nil {
61
+ fs = flag.NewFlagSet(subcommand.Name, flag.ExitOnError)
62
+ }
63
+
64
+ err := fs.Parse(os.Args[2:])
65
+ if err != nil {
66
log.Println(err)
49
- os.Exit(exitCode)
67
+ os.Exit(caddy.ExitCodeFailedStartup)
68
}
51
-}
69
53
-// commandFunc is a function that executes
54
-// a subcommand. It returns an exit code and
55
-// any associated error.
56
-type commandFunc func() (int, error)
57
-
58
-var commands = map[string]commandFunc{
59
- "start": cmdStart,
60
- "run": cmdRun,
61
- "stop": cmdStop,
62
- "reload": cmdReload,
63
- "version": cmdVersion,
64
- "list-modules": cmdListModules,
65
- "environ": cmdEnviron,
66
- "adapt-config": cmdAdaptConfig,
67
-}
70
+ exitCode, err := subcommand.Func(Flags{fs})
71
+ if err != nil {
72
+ log.Printf("%s: %v", subcommand.Name, err)
73
+ }
74
69
-func usageString() string {
70
- buf := new(bytes.Buffer)
71
- buf.WriteString("usage: caddy <command> [<args>]")
72
- flag.CommandLine.SetOutput(buf)
73
- flag.CommandLine.PrintDefaults()
74
- return buf.String()
75
+ os.Exit(exitCode)
76
}
77
78
// handlePingbackConn reads from conn and ensures it matches
@@ -156,3 +157,74 @@ func loadConfig(configFile, adapterName string) ([]byte, error) {
157
158
return config, nil
159
}
160
+
161
+// Flags wraps a FlagSet so that typed values
162
+// from flags can be easily retrieved.
163
+type Flags struct {
164
+ *flag.FlagSet
165
+}
166
+
167
+// String returns the string representation of the
168
+// flag given by name. It panics if the flag is not
169
+// in the flag set.
170
+func (f Flags) String(name string) string {
171
+ return f.FlagSet.Lookup(name).Value.String()
172
+}
173
+
174
+// Bool returns the boolean representation of the
175
+// flag given by name. It returns false if the flag
176
+// is not a boolean type. It panics if the flag is
177
+// not in the flag set.
178
+func (f Flags) Bool(name string) bool {
179
+ val, _ := strconv.ParseBool(f.String(name))
180
+ return val
181
+}
182
+
183
+// Int returns the integer representation of the
184
+// flag given by name. It returns 0 if the flag
185
+// is not an integer type. It panics if the flag is
186
+// not in the flag set.
187
+func (f Flags) Int(name string) int {
188
+ val, _ := strconv.ParseInt(f.String(name), 0, strconv.IntSize)
189
+ return int(val)
190
+}
191
+
192
+// Float64 returns the float64 representation of the
193
+// flag given by name. It returns false if the flag
194
+// is not a float63 type. It panics if the flag is
195
+// not in the flag set.
196
+func (f Flags) Float64(name string) float64 {
197
+ val, _ := strconv.ParseFloat(f.String(name), 64)
198
+ return val
199
+}
200
+
201
+// Duration returns the duration representation of the
202
+// flag given by name. It returns false if the flag
203
+// is not a duration type. It panics if the flag is
204
+// not in the flag set.
205
+func (f Flags) Duration(name string) time.Duration {
206
+ val, _ := time.ParseDuration(f.String(name))
207
+ return val
208
+}
209
+
210
+// flagHelp returns the help text for fs.
211
+func flagHelp(fs *flag.FlagSet) string {
212
+ if fs == nil {
213
+ return ""
214
+ }
215
+
216
+ // temporarily redirect output
217
+ out := fs.Output()
218
+ defer fs.SetOutput(out)
219
+
220
+ buf := new(bytes.Buffer)
221
+ fs.SetOutput(buf)
222
+ fs.PrintDefaults()
223
+ return buf.String()
224
+}
225
+
226
+func printEnvironment() {
227
+ for _, v := range os.Environ() {
228
+ fmt.Println(v)
229
+ }
230
+}