@marcel91200 / caddy Mirror · read-only / commits / 0006df60
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: 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 +}