@marcel91200 / caddy Mirror · read-only / commits / a4bdf249
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

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")