summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2019-06-28 15:39:41 -0600
committerMatthew Holt <mholt@users.noreply.github.com>2019-06-28 15:39:41 -0600
commita4bdf249dbfb0f35e0a35dad6a38db371b126800 (patch)
tree00242a860d45e741fe9543875a58a15c92e7102d /cmd
parent006dc1792f0118552f81e17cfbe2802a3180c352 (diff)
Caddy 2 gets a CLI! And admin endpoint is now configurable via JSON
Diffstat (limited to 'cmd')
-rw-r--r--cmd/caddy/main.go1
-rw-r--r--cmd/commands.go205
-rw-r--r--cmd/main.go88
-rw-r--r--cmd/proc_posix.go16
-rw-r--r--cmd/proc_windows.go15
-rw-r--r--cmd/run.go26
6 files changed, 324 insertions, 27 deletions
diff --git a/cmd/caddy/main.go b/cmd/caddy/main.go
index 463b1b9..bbb4956 100644
--- a/cmd/caddy/main.go
+++ b/cmd/caddy/main.go
@@ -5,7 +5,6 @@ import (
// this is where modules get plugged in
_ "github.com/caddyserver/caddy/modules/caddyhttp"
- _ "github.com/caddyserver/caddy/modules/caddyhttp/caddylog"
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode"
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode/brotli"
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode/gzip"
diff --git a/cmd/commands.go b/cmd/commands.go
new file mode 100644
index 0000000..4745af7
--- /dev/null
+++ b/cmd/commands.go
@@ -0,0 +1,205 @@
+package caddycmd
+
+import (
+ "crypto/rand"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/caddyserver/caddy"
+ "github.com/mitchellh/go-ps"
+)
+
+func cmdStart() (int, error) {
+ startCmd := flag.NewFlagSet("start", flag.ExitOnError)
+ startCmdConfigFlag := startCmd.String("config", "", "Configuration file")
+ startCmd.Parse(os.Args[2:])
+
+ // open a listener to which the child process will connect when
+ // it is ready to confirm that it has successfully started
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ return 1, fmt.Errorf("opening listener for success confirmation: %v", err)
+ }
+ defer ln.Close()
+
+ // craft the command with a pingback address and with a
+ // pipe for its stdin, so we can tell it our confirmation
+ // code that we expect so that some random port scan at
+ // the most unfortunate time won't fool us into thinking
+ // the child succeeded (i.e. the alternative is to just
+ // wait for any connection on our listener, but better to
+ // ensure it's the process we're expecting - we can be
+ // sure by giving it some random bytes and having it echo
+ // them back to us)
+ cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
+ if *startCmdConfigFlag != "" {
+ cmd.Args = append(cmd.Args, "--config", *startCmdConfigFlag)
+ }
+ stdinpipe, err := cmd.StdinPipe()
+ if err != nil {
+ return 1, fmt.Errorf("creating stdin pipe: %v", err)
+ }
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ // generate the random bytes we'll send to the child process
+ expect := make([]byte, 32)
+ _, err = rand.Read(expect)
+ if err != nil {
+ return 1, fmt.Errorf("generating random confirmation bytes: %v", err)
+ }
+
+ // begin writing the confirmation bytes to the child's
+ // stdin; use a goroutine since the child hasn't been
+ // started yet, and writing sychronously would result
+ // in a deadlock
+ go func() {
+ stdinpipe.Write(expect)
+ stdinpipe.Close()
+ }()
+
+ // start the process
+ err = cmd.Start()
+ if err != nil {
+ return 1, fmt.Errorf("starting caddy process: %v", err)
+ }
+
+ // there are two ways we know we're done: either
+ // the process will connect to our listener, or
+ // it will exit with an error
+ success, exit := make(chan struct{}), make(chan error)
+
+ // in one goroutine, we await the success of the child process
+ go func() {
+ for {
+ conn, err := ln.Accept()
+ if err != nil {
+ log.Println(err)
+ break
+ }
+ err = handlePingbackConn(conn, expect)
+ if err == nil {
+ close(success)
+ break
+ }
+ log.Println(err)
+ }
+ }()
+
+ // in another goroutine, we await the failure of the child process
+ go func() {
+ err = cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks
+ exit <- err // sending on separate line ensures select won't trigger until after Wait unblocks
+ }()
+
+ // when one of the goroutines unblocks, we're done and can exit
+ select {
+ case <-success:
+ fmt.Println("Successfully started Caddy")
+ case err := <-exit:
+ return 1, fmt.Errorf("caddy process exited with error: %v", err)
+ }
+
+ return 0, nil
+}
+
+func cmdRun() (int, error) {
+ runCmd := flag.NewFlagSet("run", flag.ExitOnError)
+ runCmdConfigFlag := runCmd.String("config", "", "Configuration file")
+ runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success")
+ runCmd.Parse(os.Args[2:])
+
+ // if a config file was specified for bootstrapping
+ // the server instance, load it now
+ var config []byte
+ if *runCmdConfigFlag != "" {
+ var err error
+ config, err = ioutil.ReadFile(*runCmdConfigFlag)
+ if err != nil {
+ return 1, fmt.Errorf("reading config file: %v", err)
+ }
+ }
+
+ // start the admin endpoint along with any initial config
+ err := caddy.StartAdmin(config)
+ if err != nil {
+ return 0, fmt.Errorf("starting caddy administration endpoint: %v", err)
+ }
+ defer caddy.StopAdmin()
+
+ // if we are to report to another process the successful start
+ // of the server, do so now by echoing back contents of stdin
+ if *runCmdPingbackFlag != "" {
+ confirmationBytes, err := ioutil.ReadAll(os.Stdin)
+ if err != nil {
+ return 1, fmt.Errorf("reading confirmation bytes from stdin: %v", err)
+ }
+ conn, err := net.Dial("tcp", *runCmdPingbackFlag)
+ if err != nil {
+ return 1, fmt.Errorf("dialing confirmation address: %v", err)
+ }
+ defer conn.Close()
+ _, err = conn.Write(confirmationBytes)
+ if err != nil {
+ return 1, fmt.Errorf("writing confirmation bytes to %s: %v", *runCmdPingbackFlag, err)
+ }
+ }
+
+ select {}
+}
+
+func cmdStop() (int, error) {
+ processList, err := ps.Processes()
+ if err != nil {
+ return 1, fmt.Errorf("listing processes: %v", err)
+ }
+ thisProcName := filepath.Base(os.Args[0])
+ var found bool
+ for _, p := range processList {
+ // the process we're looking for should have the same name but different PID
+ if p.Executable() == thisProcName && p.Pid() != os.Getpid() {
+ found = true
+ fmt.Printf("pid=%d\n", p.Pid())
+ fmt.Printf("Graceful stop...")
+ if err := gracefullyStopProcess(p.Pid()); err != nil {
+ return 1, err
+ }
+ }
+ }
+ if !found {
+ return 1, fmt.Errorf("Caddy is not running")
+ }
+ fmt.Println(" success")
+ return 0, nil
+}
+
+func cmdVersion() (int, error) {
+ goModule := getGoBuildModule()
+ if goModule.Sum != "" {
+ // a build with a known version will also have a checksum
+ fmt.Printf("%s (%s)\n", goModule.Version, goModule.Sum)
+ } else {
+ fmt.Println(goModule.Version)
+ }
+ return 0, nil
+}
+
+func cmdListModules() (int, error) {
+ for _, m := range caddy.Modules() {
+ fmt.Println(m)
+ }
+ return 0, nil
+}
+
+func cmdEnviron() (int, error) {
+ for _, v := range os.Environ() {
+ fmt.Println(v)
+ }
+ return 0, nil
+}
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 0000000..b9baf84
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,88 @@
+package caddycmd
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net"
+ "os"
+ "runtime/debug"
+)
+
+// Main executes the main function of the caddy command.
+func Main() {
+ if len(os.Args) <= 1 {
+ fmt.Println(usageString())
+ return
+ }
+
+ subcommand, ok := commands[os.Args[1]]
+ if !ok {
+ fmt.Printf("%q is not a valid command\n", os.Args[1])
+ os.Exit(2)
+ }
+
+ if exitCode, err := subcommand(); err != nil {
+ log.Println(err)
+ os.Exit(exitCode)
+ }
+}
+
+// commandFunc is a function that executes
+// a subcommand. It returns an exit code and
+// any associated error.
+type commandFunc func() (int, error)
+
+var commands = map[string]commandFunc{
+ "start": cmdStart,
+ "stop": cmdStop,
+ "run": cmdRun,
+ "version": cmdVersion,
+ "list-modules": cmdListModules,
+ "environ": cmdEnviron,
+}
+
+func usageString() string {
+ buf := new(bytes.Buffer)
+ buf.WriteString("usage: caddy <command> [<args>]")
+ flag.CommandLine.SetOutput(buf)
+ flag.CommandLine.PrintDefaults()
+ return buf.String()
+}
+
+// handlePingbackConn reads from conn and ensures it matches
+// the bytes in expect, or returns an error if it doesn't.
+func handlePingbackConn(conn net.Conn, expect []byte) error {
+ defer conn.Close()
+ confirmationBytes, err := ioutil.ReadAll(io.LimitReader(conn, 32))
+ if err != nil {
+ return err
+ }
+ if !bytes.Equal(confirmationBytes, expect) {
+ return fmt.Errorf("wrong confirmation: %x", confirmationBytes)
+ }
+ return nil
+}
+
+// getGoBuildModule returns the build info of Caddy
+// from debug.BuildInfo (requires Go modules). If
+// no version information is available, a non-nil
+// value will still be returned, but with an
+// unknown version.
+func getGoBuildModule() *debug.Module {
+ bi, ok := debug.ReadBuildInfo()
+ if ok {
+ // The recommended way to build Caddy involves
+ // creating a separate main module, which
+ // TODO: track related Go issue: https://github.com/golang/go/issues/29228
+ for _, mod := range bi.Deps {
+ if mod.Path == "github.com/mholt/caddy" {
+ return mod
+ }
+ }
+ }
+ return &debug.Module{Version: "unknown"}
+}
diff --git a/cmd/proc_posix.go b/cmd/proc_posix.go
new file mode 100644
index 0000000..6b71d1a
--- /dev/null
+++ b/cmd/proc_posix.go
@@ -0,0 +1,16 @@
+// +build !windows
+
+package caddycmd
+
+import (
+ "fmt"
+ "syscall"
+)
+
+func gracefullyStopProcess(pid int) error {
+ err := syscall.Kill(pid, syscall.SIGINT)
+ if err != nil {
+ return fmt.Errorf("kill: %v", err)
+ }
+ return nil
+}
diff --git a/cmd/proc_windows.go b/cmd/proc_windows.go
new file mode 100644
index 0000000..8b1bf23
--- /dev/null
+++ b/cmd/proc_windows.go
@@ -0,0 +1,15 @@
+package caddycmd
+
+import (
+ "fmt"
+ "os/exec"
+ "strconv"
+)
+
+func gracefullyStopProcess(pid int) error {
+ cmd := exec.Command("taskkill", "/pid", strconv.Itoa(pid))
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("taskkill: %v", err)
+ }
+ return nil
+}
diff --git a/cmd/run.go b/cmd/run.go
deleted file mode 100644
index 703e119..0000000
--- a/cmd/run.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package caddycmd
-
-import (
- "flag"
- "log"
-
- "github.com/caddyserver/caddy"
-)
-
-// Main executes the main function of the caddy command.
-func Main() {
- flag.Parse()
-
- err := caddy.StartAdmin(*listenAddr)
- if err != nil {
- log.Fatal(err)
- }
- defer caddy.StopAdmin()
-
- log.Println("Caddy 2 admin endpoint listening on", *listenAddr)
-
- select {}
-}
-
-// TODO: for dev only
-var listenAddr = flag.String("listen", ":1234", "The admin endpoint listener address")