diff options
-rw-r--r-- | app/app.go | 76 | ||||
-rw-r--r-- | caddy/assets/path.go | 29 | ||||
-rw-r--r-- | caddy/assets/path_test.go | 12 | ||||
-rw-r--r-- | caddy/caddy.go | 358 | ||||
-rw-r--r-- | caddy/caddy_test.go | 32 | ||||
-rw-r--r-- | caddy/caddyfile/json.go | 163 | ||||
-rw-r--r-- | caddy/caddyfile/json_test.go | 91 | ||||
-rw-r--r-- | caddy/config.go (renamed from config/config.go) | 177 | ||||
-rw-r--r-- | caddy/config_test.go (renamed from config/config_test.go) | 2 | ||||
-rw-r--r-- | caddy/directives.go (renamed from config/directives.go) | 8 | ||||
-rw-r--r-- | caddy/helpers.go | 71 | ||||
-rw-r--r-- | caddy/letsencrypt/crypto.go | 30 | ||||
-rw-r--r-- | caddy/letsencrypt/crypto_test.go | 51 | ||||
-rw-r--r-- | caddy/letsencrypt/handler.go | 67 | ||||
-rw-r--r-- | caddy/letsencrypt/letsencrypt.go | 497 | ||||
-rw-r--r-- | caddy/letsencrypt/letsencrypt_test.go | 51 | ||||
-rw-r--r-- | caddy/letsencrypt/maintain.go | 183 | ||||
-rw-r--r-- | caddy/letsencrypt/storage.go | 94 | ||||
-rw-r--r-- | caddy/letsencrypt/storage_test.go | 84 | ||||
-rw-r--r-- | caddy/letsencrypt/user.go | 196 | ||||
-rw-r--r-- | caddy/letsencrypt/user_test.go | 192 | ||||
-rw-r--r-- | caddy/parse/dispenser.go (renamed from config/parse/dispenser.go) | 0 | ||||
-rw-r--r-- | caddy/parse/dispenser_test.go (renamed from config/parse/dispenser_test.go) | 0 | ||||
-rw-r--r-- | caddy/parse/import_test1.txt (renamed from config/parse/import_test1.txt) | 0 | ||||
-rw-r--r-- | caddy/parse/import_test2.txt (renamed from config/parse/import_test2.txt) | 0 | ||||
-rw-r--r-- | caddy/parse/lexer.go (renamed from config/parse/lexer.go) | 0 | ||||
-rw-r--r-- | caddy/parse/lexer_test.go (renamed from config/parse/lexer_test.go) | 0 | ||||
-rw-r--r-- | caddy/parse/parse.go (renamed from config/parse/parse.go) | 8 | ||||
-rw-r--r-- | caddy/parse/parse_test.go (renamed from config/parse/parse_test.go) | 0 | ||||
-rw-r--r-- | caddy/parse/parsing.go (renamed from config/parse/parsing.go) | 11 | ||||
-rw-r--r-- | caddy/parse/parsing_test.go (renamed from config/parse/parsing_test.go) | 2 | ||||
-rw-r--r-- | caddy/restart.go | 102 | ||||
-rw-r--r-- | caddy/restart_windows.go | 25 | ||||
-rw-r--r-- | caddy/setup/basicauth.go (renamed from config/setup/basicauth.go) | 0 | ||||
-rw-r--r-- | caddy/setup/basicauth_test.go (renamed from config/setup/basicauth_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/bindhost.go (renamed from config/setup/bindhost.go) | 0 | ||||
-rw-r--r-- | caddy/setup/browse.go (renamed from config/setup/browse.go) | 0 | ||||
-rw-r--r-- | caddy/setup/controller.go (renamed from config/setup/controller.go) | 2 | ||||
-rw-r--r-- | caddy/setup/errors.go (renamed from config/setup/errors.go) | 0 | ||||
-rw-r--r-- | caddy/setup/errors_test.go (renamed from config/setup/errors_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/ext.go (renamed from config/setup/ext.go) | 0 | ||||
-rw-r--r-- | caddy/setup/ext_test.go (renamed from config/setup/ext_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/fastcgi.go (renamed from config/setup/fastcgi.go) | 0 | ||||
-rw-r--r-- | caddy/setup/fastcgi_test.go (renamed from config/setup/fastcgi_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/gzip.go (renamed from config/setup/gzip.go) | 0 | ||||
-rw-r--r-- | caddy/setup/gzip_test.go (renamed from config/setup/gzip_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/headers.go (renamed from config/setup/headers.go) | 0 | ||||
-rw-r--r-- | caddy/setup/headers_test.go (renamed from config/setup/headers_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/internal.go (renamed from config/setup/internal.go) | 0 | ||||
-rw-r--r-- | caddy/setup/internal_test.go (renamed from config/setup/internal_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/log.go (renamed from config/setup/log.go) | 0 | ||||
-rw-r--r-- | caddy/setup/log_test.go (renamed from config/setup/log_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/markdown.go (renamed from config/setup/markdown.go) | 0 | ||||
-rw-r--r-- | caddy/setup/markdown_test.go (renamed from config/setup/markdown_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/mime.go (renamed from config/setup/mime.go) | 0 | ||||
-rw-r--r-- | caddy/setup/mime_test.go (renamed from config/setup/mime_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/proxy.go (renamed from config/setup/proxy.go) | 0 | ||||
-rw-r--r-- | caddy/setup/redir.go (renamed from config/setup/redir.go) | 0 | ||||
-rw-r--r-- | caddy/setup/rewrite.go (renamed from config/setup/rewrite.go) | 0 | ||||
-rw-r--r-- | caddy/setup/rewrite_test.go (renamed from config/setup/rewrite_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/roller.go (renamed from config/setup/roller.go) | 0 | ||||
-rw-r--r-- | caddy/setup/root.go (renamed from config/setup/root.go) | 0 | ||||
-rw-r--r-- | caddy/setup/root_test.go (renamed from config/setup/root_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/startupshutdown.go (renamed from config/setup/startupshutdown.go) | 0 | ||||
-rw-r--r-- | caddy/setup/startupshutdown_test.go (renamed from config/setup/startupshutdown_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/templates.go (renamed from config/setup/templates.go) | 0 | ||||
-rw-r--r-- | caddy/setup/templates_test.go (renamed from config/setup/templates_test.go) | 0 | ||||
-rw-r--r-- | caddy/setup/testdata/blog/first_post.md (renamed from config/setup/testdata/blog/first_post.md) | 0 | ||||
-rw-r--r-- | caddy/setup/testdata/header.html (renamed from config/setup/testdata/header.html) | 0 | ||||
-rw-r--r-- | caddy/setup/testdata/tpl_with_include.html (renamed from config/setup/testdata/tpl_with_include.html) | 0 | ||||
-rw-r--r-- | caddy/setup/tls.go (renamed from config/setup/tls.go) | 29 | ||||
-rw-r--r-- | caddy/setup/tls_test.go (renamed from config/setup/tls_test.go) | 9 | ||||
-rw-r--r-- | caddy/setup/websocket.go (renamed from config/setup/websocket.go) | 0 | ||||
-rw-r--r-- | caddy/setup/websocket_test.go (renamed from config/setup/websocket_test.go) | 0 | ||||
-rw-r--r-- | caddy/sigtrap.go | 33 | ||||
-rw-r--r-- | caddy/sigtrap_posix.go | 43 | ||||
-rw-r--r-- | dist/CHANGES.txt | 19 | ||||
-rw-r--r-- | dist/README.txt | 2 | ||||
-rw-r--r-- | main.go | 210 | ||||
-rw-r--r-- | main_test.go (renamed from app/app_test.go) | 6 | ||||
-rw-r--r-- | middleware/proxy/upstream.go | 2 | ||||
-rw-r--r-- | server/config.go | 6 | ||||
-rw-r--r-- | server/graceful.go | 76 | ||||
-rw-r--r-- | server/server.go | 402 |
84 files changed, 3061 insertions, 390 deletions
diff --git a/app/app.go b/app/app.go deleted file mode 100644 index c495a43a4..000000000 --- a/app/app.go +++ /dev/null @@ -1,76 +0,0 @@ -// Package app holds application-global state to make it accessible -// by other packages in the application. -// -// This package differs from config in that the things in app aren't -// really related to server configuration. -package app - -import ( - "errors" - "runtime" - "strconv" - "strings" - "sync" - - "github.com/mholt/caddy/server" -) - -const ( - // Name is the program name - Name = "Caddy" - - // Version is the program version - Version = "0.7.6" -) - -var ( - // Servers is a list of all the currently-listening servers - Servers []*server.Server - - // ServersMutex protects the Servers slice during changes - ServersMutex sync.Mutex - - // Wg is used to wait for all servers to shut down - Wg sync.WaitGroup - - // HTTP2 indicates whether HTTP2 is enabled or not - HTTP2 bool // TODO: temporary flag until http2 is standard - - // Quiet mode hides non-error initialization output - Quiet bool -) - -// SetCPU parses string cpu and sets GOMAXPROCS -// according to its value. It accepts either -// a number (e.g. 3) or a percent (e.g. 50%). -func SetCPU(cpu string) error { - var numCPU int - - availCPU := runtime.NumCPU() - - if strings.HasSuffix(cpu, "%") { - // Percent - var percent float32 - pctStr := cpu[:len(cpu)-1] - pctInt, err := strconv.Atoi(pctStr) - if err != nil || pctInt < 1 || pctInt > 100 { - return errors.New("invalid CPU value: percentage must be between 1-100") - } - percent = float32(pctInt) / 100 - numCPU = int(float32(availCPU) * percent) - } else { - // Number - num, err := strconv.Atoi(cpu) - if err != nil || num < 1 { - return errors.New("invalid CPU value: provide a number or percent greater than 0") - } - numCPU = num - } - - if numCPU > availCPU { - numCPU = availCPU - } - - runtime.GOMAXPROCS(numCPU) - return nil -} diff --git a/caddy/assets/path.go b/caddy/assets/path.go new file mode 100644 index 000000000..46b883b1c --- /dev/null +++ b/caddy/assets/path.go @@ -0,0 +1,29 @@ +package assets + +import ( + "os" + "path/filepath" + "runtime" +) + +// Path returns the path to the folder +// where the application may store data. This +// currently resolves to ~/.caddy +func Path() string { + return filepath.Join(userHomeDir(), ".caddy") +} + +// userHomeDir returns the user's home directory according to +// environment variables. +// +// Credit: http://stackoverflow.com/a/7922977/1048862 +func userHomeDir() string { + if runtime.GOOS == "windows" { + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if home == "" { + home = os.Getenv("USERPROFILE") + } + return home + } + return os.Getenv("HOME") +} diff --git a/caddy/assets/path_test.go b/caddy/assets/path_test.go new file mode 100644 index 000000000..374f813af --- /dev/null +++ b/caddy/assets/path_test.go @@ -0,0 +1,12 @@ +package assets + +import ( + "strings" + "testing" +) + +func TestPath(t *testing.T) { + if actual := Path(); !strings.HasSuffix(actual, ".caddy") { + t.Errorf("Expected path to be a .caddy folder, got: %v", actual) + } +} diff --git a/caddy/caddy.go b/caddy/caddy.go new file mode 100644 index 000000000..f92666f02 --- /dev/null +++ b/caddy/caddy.go @@ -0,0 +1,358 @@ +// Package caddy implements the Caddy web server as a service. +// +// To use this package, follow a few simple steps: +// +// 1. Set the AppName and AppVersion variables. +// 2. Call LoadCaddyfile() to get the Caddyfile (it +// might have been piped in as part of a restart). +// You should pass in your own Caddyfile loader. +// 3. Call caddy.Start() to start Caddy, caddy.Stop() +// to stop it, or caddy.Restart() to restart it. +// +// You should use caddy.Wait() to wait for all Caddy servers +// to quit before your process exits. +// +// Importing this package has the side-effect of trapping +// SIGINT on all platforms and SIGUSR1 on not-Windows systems. +// It has to do this in order to perform shutdowns or reloads. +package caddy + +import ( + "bytes" + "encoding/gob" + "errors" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "path" + "strings" + "sync" + + "github.com/mholt/caddy/caddy/letsencrypt" + "github.com/mholt/caddy/server" +) + +// Configurable application parameters +var ( + // The name and version of the application. + AppName, AppVersion string + + // If true, initialization will not show any informative output. + Quiet bool + + // DefaultInput is the default configuration to use when config input is empty or missing. + DefaultInput = CaddyfileInput{ + Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", DefaultHost, DefaultPort, DefaultRoot)), + } + + // HTTP2 indicates whether HTTP2 is enabled or not + HTTP2 bool // TODO: temporary flag until http2 is standard +) + +var ( + // caddyfile is the input configuration text used for this process + caddyfile Input + + // caddyfileMu protects caddyfile during changes + caddyfileMu sync.Mutex + + // incompleteRestartErr occurs if this process is a fork + // of the parent but no Caddyfile was piped in + incompleteRestartErr = errors.New("cannot finish restart successfully") + + // servers is a list of all the currently-listening servers + servers []*server.Server + + // serversMu protects the servers slice during changes + serversMu sync.Mutex + + // wg is used to wait for all servers to shut down + wg sync.WaitGroup + + // loadedGob is used if this is a child process as part of + // a graceful restart; it is used to map listeners to their + // index in the list of inherited file descriptors. This + // variable is not safe for concurrent access. + loadedGob caddyfileGob +) + +const ( + DefaultHost = "0.0.0.0" + DefaultPort = "2015" + DefaultRoot = "." +) + +// Start starts Caddy with the given Caddyfile. If cdyfile +// is nil or the process is forked from a parent as part of +// a graceful restart, Caddy will check to see if Caddyfile +// was piped from stdin and use that. It blocks until all the +// servers are listening. +// +// If this process is a fork and no Caddyfile was piped in, +// an error will be returned (the Restart() function does this +// for you automatically). If this process is NOT a fork and +// cdyfile is nil, a default configuration will be assumed. +// In any case, an error is returned if Caddy could not be +// started. +func Start(cdyfile Input) error { + // TODO: What if already started -- is that an error? + + var err error + + // Input must never be nil; try to load something + if cdyfile == nil { + cdyfile, err = LoadCaddyfile(nil) + if err != nil { + return err + } + } + + caddyfileMu.Lock() + caddyfile = cdyfile + caddyfileMu.Unlock() + + // load the server configs (activates Let's Encrypt) + configs, err := loadConfigs(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body())) + if err != nil { + return err + } + + // group virtualhosts by address + groupings, err := arrangeBindings(configs) + if err != nil { + return err + } + + // Start each server with its one or more configurations + err = startServers(groupings) + if err != nil { + return err + } + + // Close remaining file descriptors we may have inherited that we don't need + if isRestart() { + for _, fdIndex := range loadedGob.ListenerFds { + file := os.NewFile(fdIndex, "") + fln, err := net.FileListener(file) + if err == nil { + fln.Close() + } + } + } + + // Show initialization output + if !Quiet && !isRestart() { + var checkedFdLimit bool + for _, group := range groupings { + for _, conf := range group.Configs { + // Print address of site + fmt.Println(conf.Address()) + + // Note if non-localhost site resolves to loopback interface + if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { + fmt.Printf("Notice: %s is only accessible on this machine (%s)\n", + conf.Host, group.BindAddr.IP.String()) + } + if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { + checkFdlimit() + checkedFdLimit = true + } + } + } + } + + // Tell parent process that we got this + if isRestart() { + ppipe := os.NewFile(3, "") // parent is listening on pipe at index 3 + ppipe.Write([]byte("success")) + ppipe.Close() + } + + return nil +} + +// startServers starts all the servers in groupings, +// taking into account whether or not this process is +// a child from a graceful restart or not. It blocks +// until the servers are listening. +func startServers(groupings Group) error { + var startupWg sync.WaitGroup + + for _, group := range groupings { + s, err := server.New(group.BindAddr.String(), group.Configs) + if err != nil { + log.Fatal(err) + } + s.HTTP2 = HTTP2 // TODO: This setting is temporary + + var ln server.ListenerFile + if isRestart() { + // Look up this server's listener in the map of inherited file descriptors; + // if we don't have one, we must make a new one. + if fdIndex, ok := loadedGob.ListenerFds[s.Addr]; ok { + file := os.NewFile(fdIndex, "") + + fln, err := net.FileListener(file) + if err != nil { + log.Fatal(err) + } + + ln, ok = fln.(server.ListenerFile) + if !ok { + log.Fatal("listener was not a ListenerFile") + } + + delete(loadedGob.ListenerFds, s.Addr) // mark it as used + } + } + + wg.Add(1) + go func(s *server.Server, ln server.ListenerFile) { + defer wg.Done() + + if ln != nil { + err = s.Serve(ln) + } else { + err = s.ListenAndServe() + } + + // "use of closed network connection" is normal if doing graceful shutdown... + if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { + if isRestart() { + log.Fatal(err) + } else { + log.Println(err) + } + } + }(s, ln) + + startupWg.Add(1) + go func(s *server.Server) { + defer startupWg.Done() + s.WaitUntilStarted() + }(s) + + serversMu.Lock() + servers = append(servers, s) + serversMu.Unlock() + } + + startupWg.Wait() + + return nil +} + +// Stop stops all servers. It blocks until they are all stopped. +// It does NOT execute shutdown callbacks that may have been +// configured by middleware (they are executed on SIGINT). +func Stop() error { + letsencrypt.Deactivate() + + serversMu.Lock() + for _, s := range servers { + s.Stop() // TODO: error checking/reporting? + } + servers = []*server.Server{} // don't reuse servers + serversMu.Unlock() + + return nil +} + +// Wait blocks until all servers are stopped. +func Wait() { + wg.Wait() +} + +// LoadCaddyfile loads a Caddyfile in a way that prioritizes +// reading from stdin pipe; otherwise it calls loader to load +// the Caddyfile. If loader does not return a Caddyfile, the +// default one will be returned. Thus, if there are no other +// errors, this function always returns at least the default +// Caddyfile (not the previously-used Caddyfile). +func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) { + // If we are a fork, finishing the restart is highest priority; + // piped input is required in this case. + if isRestart() { + err := gob.NewDecoder(os.Stdin).Decode(&loadedGob) + if err != nil { + return nil, err + } + cdyfile = loadedGob.Caddyfile + } + + // Otherwise, we first try to get from stdin pipe + if cdyfile == nil { + cdyfile, err = CaddyfileFromPipe(os.Stdin) + if err != nil { + return nil, err + } + } + + // No piped input, so try the user's loader instead + if cdyfile == nil && loader != nil { + cdyfile, err = loader() + } + + // Otherwise revert to default + if cdyfile == nil { + cdyfile = DefaultInput + } + + return +} + +// CaddyfileFromPipe loads the Caddyfile input from f if f is +// not interactive input. f is assumed to be a pipe or stream, +// such as os.Stdin. If f is not a pipe, no error is returned +// but the Input value will be nil. An error is only returned +// if there was an error reading the pipe, even if the length +// of what was read is 0. +func CaddyfileFromPipe(f *os.File) (Input, error) { + fi, err := f.Stat() + if err == nil && fi.Mode()&os.ModeCharDevice == 0 { + // Note that a non-nil error is not a problem. Windows + // will not create a stdin if there is no pipe, which + // produces an error when calling Stat(). But Unix will + // make one either way, which is why we also check that + // bitmask. + // BUG: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X) + confBody, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + return CaddyfileInput{ + Contents: confBody, + Filepath: f.Name(), + }, nil + } + + // not having input from the pipe is not itself an error, + // just means no input to return. + return nil, nil +} + +// Caddyfile returns the current Caddyfile +func Caddyfile() Input { + caddyfileMu.Lock() + defer caddyfileMu.Unlock() + return caddyfile +} + +// Input represents a Caddyfile; its contents and file path +// (which should include the file name at the end of the path). +// If path does not apply (e.g. piped input) you may use +// any understandable value. The path is mainly used for logging, +// error messages, and debugging. +type Input interface { + // Gets the Caddyfile contents + Body() []byte + + // Gets the path to the origin file + Path() string + + // IsFile returns true if the original input was a file on the file system + // that could be loaded again later if requested. + IsFile() bool +} diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go new file mode 100644 index 000000000..ae84b31df --- /dev/null +++ b/caddy/caddy_test.go @@ -0,0 +1,32 @@ +package caddy + +import ( + "net/http" + "testing" + "time" +) + +func TestCaddyStartStop(t *testing.T) { + caddyfile := "localhost:1984\ntls off" + + for i := 0; i < 2; i++ { + err := Start(CaddyfileInput{Contents: []byte(caddyfile)}) + if err != nil { + t.Fatalf("Error starting, iteration %d: %v", i, err) + } + + client := http.Client{ + Timeout: time.Duration(2 * time.Second), + } + resp, err := client.Get("http://localhost:1984") + if err != nil { + t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err) + } + resp.Body.Close() + + err = Stop() + if err != nil { + t.Fatalf("Error stopping, iteration %d: %v", i, err) + } + } +} diff --git a/caddy/caddyfile/json.go b/caddy/caddyfile/json.go new file mode 100644 index 000000000..42171e7a6 --- /dev/null +++ b/caddy/caddyfile/json.go @@ -0,0 +1,163 @@ +package caddyfile + +import ( + "bytes" + "encoding/json" + "fmt" + "net" + "strconv" + "strings" + + "github.com/mholt/caddy/caddy/parse" +) + +const filename = "Caddyfile" + +// ToJSON converts caddyfile to its JSON representation. +func ToJSON(caddyfile []byte) ([]byte, error) { + var j Caddyfile + + serverBlocks, err := parse.ServerBlocks(filename, bytes.NewReader(caddyfile), false) + if err != nil { + return nil, err + } + + for _, sb := range serverBlocks { + block := ServerBlock{Body: make(map[string]interface{})} + + for _, host := range sb.HostList() { + block.Hosts = append(block.Hosts, strings.TrimSuffix(host, ":")) + } + + for dir, tokens := range sb.Tokens { + disp := parse.NewDispenserTokens(filename, tokens) + disp.Next() // the first token is the directive; skip it + block.Body[dir] = constructLine(disp) + } + + j = append(j, block) + } + + result, err := json.Marshal(j) + if err != nil { + return nil, err + } + + return result, nil +} + +// constructLine transforms tokens into a JSON-encodable structure; +// but only one line at a time, to be used at the top-level of +// a server block only (where the first token on each line is a +// directive) - not to be used at any other nesting level. +func constructLine(d parse.Dispenser) interface{} { + var args []interface{} + + all := d.RemainingArgs() + for _, arg := range all { + args = append(args, arg) + } + + d.Next() + if d.Val() == "{" { + args = append(args, constructBlock(d)) + } + + return args +} + +// constructBlock recursively processes tokens into a +// JSON-encodable structure. +func constructBlock(d parse.Dispenser) interface{} { + block := make(map[string]interface{}) + + for d.Next() { + if d.Val() == "}" { + break + } + + dir := d.Val() + all := d.RemainingArgs() + + var args []interface{} + for _, arg := range all { + args = append(args, arg) + } + if d.Val() == "{" { + args = append(args, constructBlock(d)) + } + + block[dir] = args + } + + return block +} + +// FromJSON converts JSON-encoded jsonBytes to Caddyfile text +func FromJSON(jsonBytes []byte) ([]byte, error) { + var j Caddyfile + var result string + + err := json.Unmarshal(jsonBytes, &j) + if err != nil { + return nil, err + } + + for _, sb := range j { + for i, host := range sb.Hosts { + if hostname, port, err := net.SplitHostPort(host); err == nil { + if port == "http" || port == "https" { + host = port + "://" + hostname + } + } + if i > 0 { + result += ", " + } + result += strings.TrimSuffix(host, ":") + } + result += jsonToText(sb.Body, 1) + } + + return []byte(result), nil +} + +// jsonToText recursively transforms a scope of JSON into plain +// Caddyfile text. +func jsonToText(scope interface{}, depth int) string { + var result string + + switch val := scope.(type) { + case string: + if strings.ContainsAny(val, "\" \n\t\r") { + result += ` "` + strings.Replace(val, "\"", "\\\"", -1) + `"` + } else { + result += " " + val + } + case int: + result += " " + strconv.Itoa(val) + case float64: + result += " " + fmt.Sprintf("%v", val) + case bool: + result += " " + fmt.Sprintf("%t", val) + case map[string]interface{}: + result += " {\n" + for param, args := range val { + result += strings.Repeat("\t", depth) + param + result += jsonToText(args, depth+1) + "\n" + } + result += strings.Repeat("\t", depth-1) + "}" + case []interface{}: + for _, v := range val { + result += jsonToText(v, depth) + } + } + + return result +} + +type Caddyfile []ServerBlock + +type ServerBlock struct { + Hosts []string `json:"hosts"` + Body map[string]interface{} `json:"body"` +} diff --git a/caddy/caddyfile/json_test.go b/caddy/caddyfile/json_test.go new file mode 100644 index 000000000..f0848b1bd --- /dev/null +++ b/caddy/caddyfile/json_test.go @@ -0,0 +1,91 @@ +package caddyfile + +import "testing" + +var tests = []struct { + caddyfile, json string +}{ + { // 0 + caddyfile: `foo { + root /bar +}`, + json: `[{"hosts":["foo"],"body":{"root":["/bar"]}}]`, + }, + { // 1 + caddyfile: `host1, host2 { + dir { + def + } +}`, + json: `[{"hosts":["host1","host2"],"body":{"dir":[{"def":null}]}}]`, + }, + { // 2 + caddyfile: `host1, host2 { + dir abc { + def ghi + } +}`, + json: `[{"hosts":["host1","host2"],"body":{"dir":["abc",{"def":["ghi"]}]}}]`, + }, + { // 3 + caddyfile: `host1:1234, host2:5678 { + dir abc { + } +}`, + json: `[{"hosts":["host1:1234","host2:5678"],"body":{"dir":["abc",{}]}}]`, + }, + { // 4 + caddyfile: `host { + foo "bar baz" +}`, + json: `[{"hosts":["host"],"body":{"foo":["bar baz"]}}]`, + }, + { // 5 + caddyfile: `host, host:80 { + foo "bar \"baz\"" +}`, + json: `[{"hosts":["host","host:80"],"body":{"foo":["bar \"baz\""]}}]`, + }, + { // 6 + caddyfile: `host { + foo "bar +baz" +}`, + json: `[{"hosts":["host"],"body":{"foo":["bar\nbaz"]}}]`, + }, + { // 7 + caddyfile: `host { + dir 123 4.56 true +}`, + json: `[{"hosts":["host"],"body":{"dir":["123","4.56","true"]}}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...? + }, + { // 8 + caddyfile: `http://host, https://host { +}`, + json: `[{"hosts":["host:http","host:https"],"body":{}}]`, // hosts in JSON are always host:port format (if port is specified), for consistency + }, +} + +func TestToJSON(t *testing.T) { + for i, test := range tests { + output, err := ToJSON([]byte(test.caddyfile)) + if err != nil { + t.Errorf("Test %d: %v", i, err) + } + if string(output) != test.json { + t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.json, string(output)) + } + } +} + +func TestFromJSON(t *testing.T) { + for i, test := range tests { + output, err := FromJSON([]byte(test.json)) + if err != nil { + t.Errorf("Test %d: %v", i, err) + } + if string(output) != test.caddyfile { + t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.caddyfile, string(output)) + } + } +} diff --git a/config/config.go b/caddy/config.go index f1d5fcce4..0b14ca593 100644 --- a/config/config.go +++ b/caddy/config.go @@ -1,4 +1,4 @@ -package config +package caddy import ( "fmt" @@ -7,43 +7,46 @@ import ( "net" "sync" - "github.com/mholt/caddy/app" - "github.com/mholt/caddy/config/parse" - "github.com/mholt/caddy/config/setup" + "github.com/mholt/caddy/caddy/letsencrypt" + "github.com/mholt/caddy/caddy/parse" + "github.com/mholt/caddy/caddy/setup" "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/server" ) const ( - DefaultHost = "0.0.0.0" - DefaultPort = "2015" - DefaultRoot = "." - // DefaultConfigFile is the name of the configuration file that is loaded // by default if no other file is specified. DefaultConfigFile = "Caddyfile" ) -// Load reads input (named filename) and parses it, returning server -// configurations grouped by listening address. -func Load(filename string, input io.Reader) (Group, error) { +// loadConfigs reads input (named filename) and parses it, returning the +// server configurations in the order they appeared in the input. As part +// of this, it activates Let's Encrypt for the configs that are produced. +// Thus, the returned configs are already optimally configured optimally +// for HTTPS. +func loadConfigs(filename string, input io.Reader) ([]server.Config, error) { var configs []server.Config // turn off timestamp for parsing flags := log.Flags() log.SetFlags(0) - serverBlocks, err := parse.ServerBlocks(filename, input) + // Each server block represents similar hosts/addresses, since they + // were grouped together in the Caddyfile. + serverBlocks, err := parse.ServerBlocks(filename, input, true) if err != nil { return nil, err } if len(serverBlocks) == 0 { - return Default() + return []server.Config{NewDefault()}, nil } - // Each server block represents similar hosts/addresses. + var lastDirectiveIndex int // we set up directives in two parts; this stores where we left off + // Iterate each server block and make a config for each one, - // executing the directives that were parsed. + // executing the directives that were parsed in order up to the tls + // directive; this is because we must activate Let's Encrypt. for i, sb := range serverBlocks { onces := makeOnces() storages := makeStorages() @@ -55,17 +58,17 @@ func Load(filename string, input io.Reader) (Group, error) { Root: Root, Middleware: make(map[string][]middleware.Middleware), ConfigFile: filename, - AppName: app.Name, - AppVersion: app.Version, + AppName: AppName, + AppVersion: AppVersion, } // It is crucial that directives are executed in the proper order. - for _, dir := range directiveOrder { + for k, dir := range directiveOrder { // Execute directive if it is in the server block if tokens, ok := sb.Tokens[dir.name]; ok { - // Each setup function gets a controller, which is the - // server config and the dispenser containing only - // this directive's tokens. + // Each setup function gets a controller, from which setup functions + // get access to the config, tokens, and other state information useful + // to set up its own host only. controller := &setup.Controller{ Config: &config, Dispenser: parse.NewDispenserTokens(filename, tokens), @@ -81,7 +84,7 @@ func Load(filename string, input io.Reader) (Group, error) { ServerBlockHosts: sb.HostList(), ServerBlockStorage: storages[dir.name], } - + // execute setup function and append middleware handler, if any midware, err := dir.setup(controller) if err != nil { return nil, err @@ -92,20 +95,78 @@ func Load(filename string, input io.Reader) (Group, error) { } storages[dir.name] = controller.ServerBlockStorage // persist for this server block } - } - if config.Port == "" { - config.Port = Port + // Stop after TLS setup, since we need to activate Let's Encrypt before continuing; + // it makes some changes to the configs that middlewares might want to know about. + if dir.name == "tls" { + lastDirectiveIndex = k + break + } } configs = append(configs, config) } } + // Now we have all the configs, but they have only been set up to the + // point of tls. We need to activate Let's Encrypt before setting up + // the rest of the middlewares so they have correct information regarding + // TLS configuration, if necessary. (this call is append-only, so our + // iterations below shouldn't be affected) + configs, err = letsencrypt.Activate(configs) + if err != nil { + return nil, err + } + + // Finish setting up the rest of the directives, now that TLS is + // optimally configured. These loops are similar to above except + // we don't iterate all the directives from the beginning and we + // don't create new configs. + configIndex := -1 + for i, sb := range serverBlocks { + onces := makeOnces() + storages := makeStorages() + + for j := range sb.Addresses { + configIndex++ + + for k := lastDirectiveIndex + 1; k < len(directiveOrder); k++ { + dir := directiveOrder[k] + + if tokens, ok := sb.Tokens[dir.name]; ok { + controller := &setup.Controller{ + Config: &configs[configIndex], + Dispenser: parse.NewDispenserTokens(filename, tokens), + OncePerServerBlock: func(f func() error) error { + var err error + onces[dir.name].Do(func() { + err = f() + }) + return err + }, + ServerBlockIndex: i, + ServerBlockHostIndex: j, + ServerBlockHosts: sb.HostList(), + ServerBlockStorage: storages[dir.name], + } + midware, err := dir.setup(controller) + if err != nil { + return nil, err + } + if midware != nil { + // TODO: For now, we only support the default path scope / + configs[configIndex].Middleware["/"] = append(configs[configIndex].Middleware["/"], midware) + } + storages[dir.name] = controller.ServerBlockStorage // persist for this server block + } + } + } + } + // restore logging settings log.SetFlags(flags) - return arrangeBindings(configs) + return configs, nil } // makeOnces makes a map of directive name to sync.Once @@ -145,14 +206,19 @@ func makeStorages() map[string]interface{} { // bind address to list of configs that would become VirtualHosts on that // server. Use the keys of the returned map to create listeners, and use // the associated values to set up the virtualhosts. -func arrangeBindings(allConfigs []server.Config) (map[*net.TCPAddr][]server.Config, error) { - addresses := make(map[*net.TCPAddr][]server.Config) +func arrangeBindings(allConfigs []server.Config) (Group, error) { + var groupings Group // Group configs by bind address for _, conf := range allConfigs { - newAddr, warnErr, fatalErr := resolveAddr(conf) + // use default port if none is specified + if conf.Port == "" { + conf.Port = Port + } + + bindAddr, warnErr, fatalErr := resolveAddr(conf) if fatalErr != nil { - return addresses, fatalErr + return groupings, fatalErr } if warnErr != nil { log.Println("[Warning]", warnErr) @@ -161,37 +227,40 @@ func arrangeBindings(allConfigs []server.Config) (map[*net.TCPAddr][]server.Conf // Make sure to compare the string representation of the address, // not the pointer, since a new *TCPAddr is created each time. var existing bool - for addr := range addresses { - if addr.String() == newAddr.String() { - addresses[addr] = append(addresses[addr], conf) + for i := 0; i < len(groupings); i++ { + if groupings[i].BindAddr.String() == bindAddr.String() { + groupings[i].Configs = append(groupings[i].Configs, conf) existing = true break } } if !existing { - addresses[newAddr] = append(addresses[newAddr], conf) + groupings = append(groupings, BindingMapping{ + BindAddr: bindAddr, + Configs: []server.Config{conf}, + }) } } // Don't allow HTTP and HTTPS to be served on the same address - for _, configs := range addresses { - isTLS := configs[0].TLS.Enabled - for _, config := range configs { + for _, group := range groupings { + isTLS := group.Configs[0].TLS.Enabled + for _, config := range group.Configs { if config.TLS.Enabled != isTLS { thisConfigProto, otherConfigProto := "HTTP", "HTTP" if config.TLS.Enabled { thisConfigProto = "HTTPS" } - if configs[0].TLS.Enabled { + if group.Configs[0].TLS.Enabled { otherConfigProto = "HTTPS" } - return addresses, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address", - configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto) + return groupings, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address", + group.Configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto) } } } - return addresses, nil + return groupings, nil } // resolveAddr determines the address (host and port) that a config will @@ -209,6 +278,7 @@ func arrangeBindings(allConfigs []server.Config) (map[*net.TCPAddr][]server.Conf func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr error, fatalErr error) { bindHost := conf.BindHost + // TODO: Do we even need the port? Maybe we just need to look up the host. resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(bindHost, conf.Port)) if warnErr != nil { // Most likely the host lookup failed or the port is unknown @@ -265,18 +335,27 @@ func NewDefault() server.Config { } } -// Default obtains a default config and arranges -// bindings so it's ready to use. -func Default() (Group, error) { - return arrangeBindings([]server.Config{NewDefault()}) -} - -// These three defaults are configurable through the command line +// These defaults are configurable through the command line var ( + // Site root Root = DefaultRoot + + // Site host Host = DefaultHost + + // Site port Port = DefaultPort ) +// BindingMapping maps a network address to configurations +// that will bind to it. The order of the configs is important. +type BindingMapping struct { + BindAddr *net.TCPAddr + Configs []server.Config +} + // Group maps network addresses to their configurations. -type Group map[*net.TCPAddr][]server.Config +// Preserving the order of the groupings is important +// (related to graceful shutdown and restart) +// so this is a slice, not a literal map. +type Group []BindingMapping diff --git a/config/config_test.go b/caddy/config_test.go index 7e4763593..3df0b5cc9 100644 --- a/config/config_test.go +++ b/caddy/config_test.go @@ -1,4 +1,4 @@ -package config +package caddy import ( "reflect" diff --git a/config/directives.go b/caddy/directives.go index 354b55959..542985022 100644 --- a/config/directives.go +++ b/caddy/directives.go @@ -1,8 +1,8 @@ -package config +package caddy import ( - "github.com/mholt/caddy/config/parse" - "github.com/mholt/caddy/config/setup" + "github.com/mholt/caddy/caddy/parse" + "github.com/mholt/caddy/caddy/setup" "github.com/mholt/caddy/middleware" ) @@ -42,7 +42,7 @@ func init() { var directiveOrder = []directive{ // Essential directives that initialize vital configuration settings {"root", setup.Root}, - {"tls", setup.TLS}, + {"tls", setup.TLS}, // letsencrypt is set up just after tls {"bind", setup.BindHost}, // Other directives that don't create HTTP handlers diff --git a/caddy/helpers.go b/caddy/helpers.go new file mode 100644 index 000000000..66446a6c0 --- /dev/null +++ b/caddy/helpers.go @@ -0,0 +1,71 @@ +package caddy + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + + "github.com/mholt/caddy/caddy/letsencrypt" +) + +func init() { + letsencrypt.OnChange = func() error { return Restart(nil) } +} + +// isLocalhost returns true if host looks explicitly like a localhost address. +func isLocalhost(host string) bool { + return host == "localhost" || host == "::1" || strings.HasPrefix(host, "127.") +} + +// checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum. +func checkFdlimit() { + const min = 4096 + + // Warn if ulimit is too low for production sites + if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { + out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH + if err == nil { + // Note that an error here need not be reported + lim, err := strconv.Atoi(string(bytes.TrimSpace(out))) + if err == nil && lim < min { + fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min) + } + } + } +} + +// caddyfileGob maps bind address to index of the file descriptor +// in the Files array passed to the child process. It also contains +// the caddyfile contents. Used only during graceful restarts. +type caddyfileGob struct { + ListenerFds map[string]uintptr + Caddyfile Input +} + +// isRestart returns whether this process is, according +// to env variables, a fork as part of a graceful restart. +func isRestart() bool { + return os.Getenv("CADDY_RESTART") == "true" +} + +// CaddyfileInput represents a Caddyfile as input +// and is simply a convenient way to implement +// the Input interface. +type CaddyfileInput struct { + Filepath string + Contents []byte + RealFile bool +} + +// Body returns c.Contents. +func (c CaddyfileInput) Body() []byte { return c.Contents } + +// Path returns c.Filepath. +func (c CaddyfileInput) Path() string { return c.Filepath } + +// Path returns true if the original input was a real file on the file system. +func (c CaddyfileInput) IsFile() bool { return c.RealFile } diff --git a/caddy/letsencrypt/crypto.go b/caddy/letsencrypt/crypto.go new file mode 100644 index 000000000..3322cc1ca --- /dev/null +++ b/caddy/letsencrypt/crypto.go @@ -0,0 +1,30 @@ +package letsencrypt + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "io/ioutil" + "os" +) + +// loadRSAPrivateKey loads a PEM-encoded RSA private key from file. +func loadRSAPrivateKey(file string) (*rsa.PrivateKey, error) { + keyBytes, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + keyBlock, _ := pem.Decode(keyBytes) + return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) +} + +// saveRSAPrivateKey saves a PEM-encoded RSA private key to file. +func saveRSAPrivateKey(key *rsa.PrivateKey, file string) error { + pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} + keyOut, err := os.Create(file) + if err != nil { + return err + } + defer keyOut.Close() + return pem.Encode(keyOut, &pemKey) +} diff --git a/caddy/letsencrypt/crypto_test.go b/caddy/letsencrypt/crypto_test.go new file mode 100644 index 000000000..7f791a6c3 --- /dev/null +++ b/caddy/letsencrypt/crypto_test.go @@ -0,0 +1,51 @@ +package letsencrypt + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "os" + "testing" +) + +func init() { + rsaKeySizeToUse = 128 // make tests faster; small key size OK for testing +} + +func TestSaveAndLoadRSAPrivateKey(t *testing.T) { + keyFile := "test.key" + defer os.Remove(keyFile) + + privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse) + if err != nil { + t.Fatal(err) + } + + // test save + err = saveRSAPrivateKey(privateKey, keyFile) + if err != nil { + t.Fatal("error saving private key:", err) + } + + // test load + loadedKey, err := loadRSAPrivateKey(keyFile) + if err != nil { + t.Error("error loading private key:", err) + } + + // very loaded key is correct + if !rsaPrivateKeysSame(privateKey, loadedKey) { + t.Error("Expected key bytes to be the same, but they weren't") + } +} + +// rsaPrivateKeyBytes returns the bytes of DER-encoded key. +func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte { + return x509.MarshalPKCS1PrivateKey(key) +} + +// rsaPrivateKeysSame compares the bytes of a and b and returns true if they are the same. +func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool { + return bytes.Equal(rsaPrivateKeyBytes(a), rsaPrivateKeyBytes(b)) +} diff --git a/caddy/letsencrypt/handler.go b/caddy/letsencrypt/handler.go new file mode 100644 index 000000000..c97d47dfb --- /dev/null +++ b/caddy/letsencrypt/handler.go @@ -0,0 +1,67 @@ +package letsencrypt + +import ( + "crypto/tls" + "net/http" + "net/http/httputil" + "net/url" + "sync" + "sync/atomic" + + "github.com/mholt/caddy/middleware" +) + +// Handler is a Caddy middleware that can proxy ACME requests +// to the real ACME endpoint. This is necessary to renew certificates +// while the server is running. Obviously, a site served on port +// 443 (HTTPS) binds to that port, so another listener created by +// our acme client can't bind successfully and solve the challenge. +// Thus, we chain this handler in so that it can, when activated, +// proxy ACME requests to an ACME client listening on an alternate +// port. +type Handler struct { + sync.Mutex // protects the ChallengePath property + Next middleware.Handler + ChallengeActive int32 // use sync/atomic for speed to set/get this flag + ChallengePath string // the exact request path to match before proxying +} + +// ServeHTTP is basically a no-op unless an ACME challenge is active on this host +// and the request path matches the expected path exactly. +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + // Only if challenge is active + if atomic.LoadInt32(&h.ChallengeActive) == 1 { + h.Lock() + path := h.ChallengePath + h.Unlock() + + // Request path must be correct; if so, proxy to ACME client + if r.URL.Path == path { + upstream, err := url.Parse("https://" + r.Host + ":" + alternatePort) + if err != nil { + return http.StatusInternalServerError, err + } + proxy := httputil.NewSingleHostReverseProxy(upstream) + proxy.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // client uses self-signed cert + } + proxy.ServeHTTP(w, r) + return 0, nil + } + } + + return h.Next.ServeHTTP(w, r) +} + +// ChallengeOn enables h to proxy ACME requests. +func (h *Handler) ChallengeOn(challengePath string) { + h.Lock() + h.ChallengePath = challengePath + h.Unlock() + atomic.StoreInt32(&h.ChallengeActive, 1) +} + +// ChallengeOff disables ACME proxying from this h. +func (h *Handler) ChallengeOff(success bool) { + atomic.StoreInt32(&h.ChallengeActive, 0) +} diff --git a/caddy/letsencrypt/letsencrypt.go b/caddy/letsencrypt/letsencrypt.go new file mode 100644 index 000000000..1b73fb6e0 --- /dev/null +++ b/caddy/letsencrypt/letsencrypt.go @@ -0,0 +1,497 @@ +// Package letsencrypt integrates Let's Encrypt functionality into Caddy +// with first-class support for creating and renewing certificates +// automatically. It is designed to configure sites for HTTPS by default. +package letsencrypt + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "os" + "strings" + "time" + + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/redirect" + "github.com/mholt/caddy/server" + "github.com/xenolf/lego/acme" +) + +// Activate sets up TLS for each server config in configs +// as needed. It only skips the config if the cert and key +// are already provided, if plaintext http is explicitly +// specified as the port, TLS is explicitly disabled, or +// the host looks like a loopback or wildcard address. +// +// This function may prompt the user to provide an email +// address if none is available through other means. It +// prefers the email address specified in the config, but +// if that is not available it will check the command line +// argument. If absent, it will use the most recent email +// address from last time. If there isn't one, the user +// will be prompted and shown SA link. +// +// Also note that calling this function activates asset +// management automatically, which keeps certificates +// renewed and OCSP stapling updated. This has the effect +// of causing restarts when assets are updated. +// +// Activate returns the updated list of configs, since +// some may have been appended, for example, to redirect +// plaintext HTTP requests to their HTTPS counterpart. +// This function only appends; it does not prepend or splice. +func Activate(configs []server.Config) ([]server.Config, error) { + // just in case previous caller forgot... + Deactivate() + + // TODO: All the output the end user should see when running caddy is something + // simple like "Setting up HTTPS..." (and maybe 'done' at the end of the line when finished). + // In other words, hide all the other logging except for on errors. Or maybe + // have a place to put those logs. + + // reset cached ocsp statuses from any previous activations + ocspStatus = make(map[*[]byte]int) + + // Identify and configure any eligible hosts for which + // we already have certs and keys in storage from last time. + configLen := len(configs) // avoid infinite loop since this loop appends plaintext to the slice + for i := 0; i < configLen; i++ { + if existingCertAndKey(configs[i].Host) && configQualifies(configs[i], configs) { + configs = autoConfigure(&configs[i], configs) + } + } + + // Group configs by email address; only configs that are eligible + // for TLS management are included. We group by email so that we + // can request certificates in batches with the same client. + // Note: The return value is a map, and iteration over a map is + // not ordered. I don't think it will be a problem, but if an + // ordering problem arises, look at this carefully. + groupedConfigs, err := groupConfigsByEmail(configs) + if err != nil { + return configs, err + } + + // obtain certificates for configs that need one, and reconfigure each + // config to use the certificates + for leEmail, serverConfigs := range groupedConfigs { + // make client to service this email address with CA server + client, err := newClient(leEmail) + if err != nil { + return configs, errors.New("error creating client: " + err.Error()) + } + + // client is ready, so let's get free, trusted SSL certificates! + Obtain: + certificates, failures := obtainCertificates(client, serverConfigs) + if len(failures) > 0 { + // Build an error string to return, using all the failures in the list. + var errMsg string + + // If an error is because of updated SA, only prompt user for agreement once + var promptedForAgreement bool + + for domain, obtainErr := range failures { + // If the failure was simply because the terms have changed, re-prompt and re-try + if tosErr, ok := obtainErr.(acme.TOSError); ok { + if !Agreed && !promptedForAgreement { + Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL + promptedForAgreement = true + } + if Agreed { + err := client.AgreeToTOS() + if err != nil { + return configs, errors.New("error agreeing to updated terms: " + err.Error()) + } + goto Obtain + } + } + + // If user did not agree or it was any other kind of error, just append to the list of errors + errMsg += "[" + domain + "] failed to get certificate: " + obtainErr.Error() + "\n" + } + + return configs, errors.New(errMsg) + } + + // ... that's it. save the certs, keys, and metadata files to disk + err = saveCertsAndKeys(certificates) + if err != nil { + return configs, errors.New("error saving assets: " + err.Error()) + } + + // it all comes down to this: turning on TLS with all the new certs + for i := 0; i < len(serverConfigs); i++ { + configs = autoConfigure(serverConfigs[i], configs) + } + } + + // renew all certificates that need renewal + renewCertificates(configs, false) + + // keep certificates renewed and OCSP stapling updated + go maintainAssets(configs, stopChan) + + return configs, nil +} + +// Deactivate cleans up long-term, in-memory resources +// allocated by calling Activate(). Essentially, it stops +// the asset maintainer from running, meaning that certificates +// will not be renewed, OCSP staples will not be updated, etc. +func Deactivate() (err error) { + defer func() { + if rec := recover(); rec != nil { + err = errors.New("already deactivated") + } + }() + close(stopChan) + stopChan = make(chan struct{}) + return +} + +// configQualifies returns true if cfg qualifes for automatic LE activation, +// but it does require the list of all configs to be passed in as well. +// It does NOT check to see if a cert and key already exist for cfg. +func configQualifies(cfg server.Config, allConfigs []server.Config) bool { + return cfg.TLS.Certificate == "" && // user could provide their own cert and key + cfg.TLS.Key == "" && + + // user can force-disable automatic HTTPS for this host + cfg.Port != "http" && + cfg.TLS.LetsEncryptEmail != "off" && + + // obviously we get can't certs for loopback or internal hosts + cfg.Host != "localhost" && + cfg.Host != "" && + cfg.Host != "0.0.0.0" && + cfg.Host != "::1" && + !strings.HasPrefix(cfg.Host, "127.") && // to use a boulder on your own machine, add fake domain to hosts file + // not excluding 10.* and 192.168.* hosts for possibility of running internal Boulder instance + + // make sure an HTTPS version of this config doesn't exist in the list already + !hostHasOtherScheme(cfg.Host, "https", allConfigs) +} + +// groupConfigsByEmail groups configs by user email address. The returned map is +// a map of email address to the configs that are serviced under that account. +// If an email address is not available for an eligible config, the user will be +// prompted to provide one. The returned map contains pointers to the original +// server config values. +func groupConfigsByEmail(configs []server.Config) (map[string][]*server.Config, error) { + initMap := make(map[string][]*server.Config) + for i := 0; i < len(configs); i++ { + // filter out configs that we already have certs for and + // that we won't be obtaining certs for - this way we won't + // bother the user for an email address unnecessarily and + // we don't obtain new certs for a host we already have certs for. + if existingCertAndKey(configs[i].Host) || !configQualifies(configs[i], configs) { + continue + } + leEmail := getEmail(configs[i]) + initMap[leEmail] = append(initMap[leEmail], &configs[i]) + } + return initMap, nil +} + +// existingCertAndKey returns true if the host has a certificate +// and private key in storage already, false otherwise. +func existingCertAndKey(host string) bool { + _, err := os.Stat(storage.SiteCertFile(host)) + if err != nil { + return false + } + _, err = os.Stat(storage.SiteKeyFile(host)) + if err != nil { + return false + } + return true +} + +// newClient creates a new ACME client to facilitate communication +// with the Let's Encrypt CA server on behalf of the user specified +// by leEmail. As part of this process, a user will be loaded from +// disk (if already exists) or created new and registered via ACME +// and saved to the file system for next time. +func newClient(leEmail string) (*acme.Client, error) { + return newClientPort(leEmail, exposePort) +} + +// newClientPort does the same thing as newClient, except it creates a +// new client with a custom port used for ACME transactions instead of +// the default port. This is important if the default port is already in +// use or is not exposed to the public, etc. +func newClientPort(leEmail, port string) (*acme.Client, error) { + // Look up or create the LE user account + leUser, err := getUser(leEmail) + if err != nil { + return nil, err + } + + // The client facilitates our communication with the CA server. + client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse, port) + if err != nil { + return nil, err + } + + // If not registered, the user must register an account with the CA + // and agree to terms + if leUser.Registration == nil { + reg, err := client.Register() + if err != nil { + return nil, errors.New("registration error: " + err.Error()) + } + leUser.Registration = reg + + if !Agreed && reg.TosURL == "" { + Agreed = promptUserAgreement(saURL, false) // TODO - latest URL + } + if !Agreed && reg.TosURL == "" { + return nil, errors.New("user must agree to terms") + } + + err = client.AgreeToTOS() + if err != nil { + saveUser(leUser) // TODO: Might as well try, right? Error check? + return nil, errors.New("error agreeing to terms: " + err.Error()) + } + + // save user to the file system + err = saveUser(leUser) + if err != nil { + return nil, errors.New("could not save user: " + err.Error()) + } + } + + return client, nil +} + +// obtainCertificates obtains certificates from the CA server for +// the configurations in serverConfigs using client. +func obtainCertificates(client *acme.Client, serverConfigs []*server.Config) ([]acme.CertificateResource, map[string]error) { + // collect all the hostnames into one slice + var hosts []string + for _, cfg := range serverConfigs { + hosts = append(hosts, cfg.Host) + } + + return client.ObtainCertificates(hosts, true) +} + +// saveCertificates saves each certificate resource to disk. This +// includes the certificate file itself, the private key, and the +// metadata file. +func saveCertsAndKeys(certificates []acme.CertificateResource) error { + for _, cert := range certificates { + os.MkdirAll(storage.Site(cert.Domain), 0700) + + // Save cert + err := ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600) + if err != nil { + return err + } + + // Save private key + err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600) + if err != nil { + return err + } + + // Save cert metadata + jsonBytes, err := json.MarshalIndent(&cert, "", "\t") + if err != nil { + return err + } + err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600) + if err != nil { + return err + } + } + return nil +} + +// autoConfigure enables TLS on cfg and appends, if necessary, a new config +// to allConfigs that redirects plaintext HTTP to its new HTTPS counterpart. +// It expects the certificate and key to already be in storage. It returns +// the new list of allConfigs, since it may append a new config. This function +// assumes that cfg was already set up for HTTPS. +func autoConfigure(cfg *server.Config, allConfigs []server.Config) []server.Config { + bundleBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host)) + // TODO: Handle these errors better + if err == nil { + ocsp, status, err := acme.GetOCSPForCert(bundleBytes) + ocspStatus[&bundleBytes] = status + if err == nil && status == acme.OCSPGood { + cfg.TLS.OCSPStaple = ocsp + } + } + cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host) + cfg.TLS.Key = storage.SiteKeyFile(cfg.Host) + cfg.TLS.Enabled = true + if cfg.Port == "" { + cfg.Port = "https" + } + + // Chain in ACME middleware proxy if we use up the SSL port + if cfg.Port == "https" || cfg.Port == "443" { + handler := new(Handler) + mid := func(next middleware.Handler) middleware.Handler { + handler.Next = next + return handler + } + cfg.Middleware["/"] = append(cfg.Middleware["/"], mid) + acmeHandlers[cfg.Host] = handler + } + + // Set up http->https redirect as long as there isn't already + // a http counterpart in the configs + if !hostHasOtherScheme(cfg.Host, "http", allConfigs) { + allConfigs = append(allConfigs, redirPlaintextHost(*cfg)) + } + + return allConfigs +} + +// hostHasOtherScheme tells you whether there is another config in the list +// for the same host but with the port equal to scheme. For example, to see +// if example.com has a https variant already, pass in example.com and +// "https" along with the list of configs. This function considers "443" +// and "https" to be the same scheme, as well as "http" and "80". +func hostHasOtherScheme(host, scheme string, allConfigs []server.Config) bool { + if scheme == "80" { + scheme = "http" + } else if scheme == "443" { + scheme = "https" + } + for _, otherCfg := range allConfigs { + if otherCfg.Host == host { + if (otherCfg.Port == scheme) || + (scheme == "https" && otherCfg.Port == "443") || + (scheme == "http" && otherCfg.Port == "80") { + return true + } + } + } + return false +} + +// redirPlaintextHost returns a new plaintext HTTP configuration for +// a virtualHost that simply redirects to cfg, which is assumed to +// be the HTTPS configuration. The returned configuration is set +// to listen on the "http" port (port 80). +func redirPlaintextHost(cfg server.Config) server.Config { + toUrl := "https://" + cfg.Host + if cfg.Port != "https" && cfg.Port != "http" { + toUrl += ":" + cfg.Port + } + + redirMidware := func(next middleware.Handler) middleware.Handler { + return redirect.Redirect{Next: next, Rules: []redirect.Rule{ + { + FromScheme: "http", + FromPath: "/", + To: toUrl + "{uri}", + Code: http.StatusMovedPermanently, + }, + }} + } + + return server.Config{ + Host: cfg.Host, + Port: "http", + Middleware: map[string][]middleware.Middleware{ + "/": []middleware.Middleware{redirMidware}, + }, + } +} + +// Revoke revokes the certificate for host via ACME protocol. +func Revoke(host string) error { + if !existingCertAndKey(host) { + return errors.New("no certificate and key for " + host) + } + + email := getEmail(server.Config{Host: host}) + if email == "" { + return errors.New("email is required to revoke") + } + + client, err := newClient(email) + if err != nil { + return err + } + + certFile := storage.SiteCertFile(host) + certBytes, err := ioutil.ReadFile(certFile) + if err != nil { + return err + } + + err = client.RevokeCertificate(certBytes) + if err != nil { + return err + } + + err = os.Remove(certFile) + if err != nil { + return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error()) + } + + return nil +} + +var ( + // Let's Encrypt account email to use if none provided + DefaultEmail string + + // Whether user has agreed to the Let's Encrypt SA + Agreed bool + + // The base URL to the CA's ACME endpoint + CAUrl string +) + +// Some essential values related to the Let's Encrypt process +const ( + // The port to expose to the CA server for Simple HTTP Challenge. + // NOTE: Let's Encrypt requires port 443. If exposePort is not 443, + // then port 443 must be forwarded to exposePort. + exposePort = "443" + + // If port 443 is in use by a Caddy server instance, then this is + // port on which the acme client will solve challenges. (Whatever is + // listening on port 443 must proxy ACME requests to this port.) + alternatePort = "5033" + + // How often to check certificates for renewal. + renewInterval = 24 * time.Hour + + // How often to update OCSP stapling. + ocspInterval = 1 * time.Hour +) + +// KeySize represents the length of a key in bits. +type KeySize int + +// Key sizes are used to determine the strength of a key. +const ( + ECC_224 KeySize = 224 + ECC_256 = 256 + RSA_2048 = 2048 + RSA_4096 = 4096 +) + +// rsaKeySizeToUse is the size to use for new RSA keys. +// This shouldn't need to change except for in tests; +// the size can be drastically reduced for speed. +var rsaKeySizeToUse = RSA_2048 + +// stopChan is used to signal the maintenance goroutine +// to terminate. +var stopChan chan struct{} + +// ocspStatus maps certificate bundle to OCSP status at start. +// It is used during regular OCSP checks to see if the OCSP +// status has changed. +var ocspStatus = make(map[*[]byte]int) diff --git a/caddy/letsencrypt/letsencrypt_test.go b/caddy/letsencrypt/letsencrypt_test.go new file mode 100644 index 000000000..841ec54bc --- /dev/null +++ b/caddy/letsencrypt/letsencrypt_test.go @@ -0,0 +1,51 @@ +package letsencrypt + +import ( + "net/http" + "testing" + + "github.com/mholt/caddy/middleware/redirect" + "github.com/mholt/caddy/server" +) + +func TestRedirPlaintextHost(t *testing.T) { + cfg := redirPlaintextHost(server.Config{ + Host: "example.com", + Port: "http", + }) + + // Check host and port + if actual, expected := cfg.Host, "example.com"; actual != expected { + t.Errorf("Expected redir config to have host %s but got %s", expected, actual) + } + if actual, expected := cfg.Port, "http"; actual != expected { + t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual) + } + + // Make sure redirect handler is set up properly + if cfg.Middleware == nil || len(cfg.Middleware["/"]) != 1 { + t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.Middleware) + } + + handler, ok := cfg.Middleware["/"][0](nil).(redirect.Redirect) + if !ok { + t.Fatalf("Expected a redirect.Redirect middleware, but got: %#v", handler) + } + if len(handler.Rules) != 1 { + t.Fatalf("Expected one redirect rule, got: %#v", handler.Rules) + } + + // Check redirect rule for correctness + if actual, expected := handler.Rules[0].FromScheme, "http"; actual != expected { + t.Errorf("Expected redirect rule to be from scheme '%s' but is actually from '%s'", expected, actual) + } + if actual, expected := handler.Rules[0].FromPath, "/"; actual != expected { + t.Errorf("Expected redirect rule to be for path '%s' but is actually for '%s'", expected, actual) + } + if actual, expected := handler.Rules[0].To, "https://example.com{uri}"; actual != expected { + t.Errorf("Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual) + } + if actual, expected := handler.Rules[0].Code, http.StatusMovedPermanently; actual != expected { + t.Errorf("Expected redirect rule to have code %d but was %d", expected, actual) + } +} diff --git a/caddy/letsencrypt/maintain.go b/caddy/letsencrypt/maintain.go new file mode 100644 index 000000000..62c714282 --- /dev/null +++ b/caddy/letsencrypt/maintain.go @@ -0,0 +1,183 @@ +package letsencrypt + +import ( + "encoding/json" + "io/ioutil" + "log" + "time" + + "github.com/mholt/caddy/server" + "github.com/xenolf/lego/acme" +) + +// OnChange is a callback function that will be used to restart +// the application or the part of the application that uses +// the certificates maintained by this package. When at least +// one certificate is renewed or an OCSP status changes, this +// function will be called. +var OnChange func() error + +// maintainAssets is a permanently-blocking function +// that loops indefinitely and, on a regular schedule, checks +// certificates for expiration and initiates a renewal of certs +// that are expiring soon. It also updates OCSP stapling and +// performs other maintenance of assets. +// +// You must pass in the server configs to maintain and the channel +// which you'll close when maintenance should stop, to allow this +// goroutine to clean up after itself and unblock. +func maintainAssets(configs []server.Config, stopChan chan struct{}) { + renewalTicker := time.NewTicker(renewInterval) + ocspTicker := time.NewTicker(ocspInterval) + + for { + select { + case <-renewalTicker.C: + n, errs := renewCertificates(configs, true) + if len(errs) > 0 { + for _, err := range errs { + log.Printf("[ERROR] cert renewal: %v\n", err) + } + } + // even if there was an error, some renewals may have succeeded + if n > 0 && OnChange != nil { + err := OnChange() + if err != nil { + log.Printf("[ERROR] onchange after cert renewal: %v\n", err) + } + } + case <-ocspTicker.C: + for bundle, oldStatus := range ocspStatus { + _, newStatus, err := acme.GetOCSPForCert(*bundle) + if err == nil && newStatus != oldStatus && OnChange != nil { + log.Printf("[INFO] ocsp status changed from %v to %v\n", oldStatus, newStatus) + err := OnChange() + if err != nil { + log.Printf("[ERROR] onchange after ocsp update: %v\n", err) + } + break + } + } + case <-stopChan: + renewalTicker.Stop() + ocspTicker.Stop() + return + } + } +} + +// renewCertificates loops through all configured site and +// looks for certificates to renew. Nothing is mutated +// through this function; all changes happen directly on disk. +// It returns the number of certificates renewed and any errors +// that occurred. It only performs a renewal if necessary. +// If useCustomPort is true, a custom port will be used, and +// whatever is listening at 443 better proxy ACME requests to it. +// Otherwise, the acme package will create its own listener on 443. +func renewCertificates(configs []server.Config, useCustomPort bool) (int, []error) { + log.Print("[INFO] Processing certificate renewals...") + var errs []error + var n int + + defer func() { + // reset these so as to not interfere with other challenges + acme.OnSimpleHTTPStart = nil + acme.OnSimpleHTTPEnd = nil + }() + + for _, cfg := range configs { + // Host must be TLS-enabled and have existing assets managed by LE + if !cfg.TLS.Enabled || !existingCertAndKey(cfg.Host) { + continue + } + + // Read the certificate and get the NotAfter time. + certBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host)) + if err != nil { + errs = append(errs, err) + continue // still have to check other certificates + } + expTime, err := acme.GetPEMCertExpiration(certBytes) + if err != nil { + errs = append(errs, err) + continue + } + + // The time returned from the certificate is always in UTC. + // So calculate the time left with local time as UTC. + // Directly convert it to days for the following checks. + daysLeft := int(expTime.Sub(time.Now().UTC()).Hours() / 24) + + // Renew with two weeks or less remaining. + if daysLeft <= 14 { + log.Printf("[INFO] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host) + var client *acme.Client + if useCustomPort { + client, err = newClientPort("", alternatePort) // email not used for renewal + } else { + client, err = newClient("") + } + if err != nil { + errs = append(errs, err) + continue + } + + // Read metadata + metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(cfg.Host)) + if err != nil { + errs = append(errs, err) + continue + } + + privBytes, err := ioutil.ReadFile(storage.SiteKeyFile(cfg.Host)) + if err != nil { + errs = append(errs, err) + continue + } + + var certMeta acme.CertificateResource + err = json.Unmarshal(metaBytes, &certMeta) + certMeta.Certificate = certBytes + certMeta.PrivateKey = privBytes + + // Tell the handler to accept and proxy acme request in order to solve challenge + acme.OnSimpleHTTPStart = acmeHandlers[cfg.Host].ChallengeOn + acme.OnSimpleHTTPEnd = acmeHandlers[cfg.Host].ChallengeOff + + // Renew certificate. + // TODO: revokeOld should be an option in the caddyfile + // TODO: bundle should be an option in the caddyfile as well :) + Renew: + newCertMeta, err := client.RenewCertificate(certMeta, true, true) + if err != nil { + if _, ok := err.(acme.TOSError); ok { + err := client.AgreeToTOS() + if err != nil { + errs = append(errs, err) + } + goto Renew + } + + time.Sleep(10 * time.Second) + newCertMeta, err = client.RenewCertificate(certMeta, true, true) + if err != nil { + errs = append(errs, err) + continue + } + } + + saveCertsAndKeys([]acme.CertificateResource{newCertMeta}) + n++ + } else if daysLeft <= 30 { + // Warn on 30 days remaining. TODO: Just do this once... + log.Printf("[WARN] There are %d days left on the certificate for %s. Will renew when 14 days remain.\n", daysLeft, cfg.Host) + } + } + + return n, errs +} + +// acmeHandlers is a map of host to ACME handler. These +// are used to proxy ACME requests to the ACME client +// when port 443 is in use. +var acmeHandlers = make(map[string]*Handler) diff --git a/caddy/letsencrypt/storage.go b/caddy/letsencrypt/storage.go new file mode 100644 index 000000000..81c0aaea9 --- /dev/null +++ b/caddy/letsencrypt/storage.go @@ -0,0 +1,94 @@ +package letsencrypt + +import ( + "path/filepath" + "strings" + + "github.com/mholt/caddy/caddy/assets" +) + +// storage is used to get file paths in a consistent, +// cross-platform way for persisting Let's Encrypt assets +// on the file system. +var storage = Storage(filepath.Join(assets.Path(), "letsencrypt")) + +// Storage is a root directory and facilitates +// forming file paths derived from it. +type Storage string + +// Sites gets the directory that stores site certificate and keys. +func (s Storage) Sites() string { + return filepath.Join(string(s), "sites") +} + +// Site returns the path to the folder containing assets for domain. +func (s Storage) Site(domain string) string { + return filepath.Join(s.Sites(), domain) +} + +// CertFile returns the path to the certificate file for domain. +func (s Storage) SiteCertFile(domain string) string { + return filepath.Join(s.Site(domain), domain+".crt") +} + +// SiteKeyFile returns the path to domain's private key file. +func (s Storage) SiteKeyFile(domain string) string { + return filepath.Join(s.Site(domain), domain+".key") +} + +// SiteMetaFile returns the path to the domain's asset metadata file. +func (s Storage) SiteMetaFile(domain string) string { + return filepath.Join(s.Site(domain), domain+".json") +} + +// Users gets the directory that stores account folders. +func (s Storage) Users() string { + return filepath.Join(string(s), "users") +} + +// User gets the account folder for the user with email. +func (s Storage) User(email string) string { + if email == "" { + email = emptyEmail + } + return filepath.Join(s.Users(), email) +} + +// UserRegFile gets the path to the registration file for +// the user with the given email address. +func (s Storage) UserRegFile(email string) string { + if email == "" { + email = emptyEmail + } + fileName := emailUsername(email) + if fileName == "" { + fileName = "registration" + } + return filepath.Join(s.User(email), fileName+".json") +} + +// UserKeyFile gets the path to the private key file for +// the user with the given email address. +func (s Storage) UserKeyFile(email string) string { + if email == "" { + email = emptyEmail + } + fileName := emailUsername(email) + if fileName == "" { + fileName = "private" + } + return filepath.Join(s.User(email), fileName+".key") +} + +// emailUsername returns the username portion of an +// email address (part before '@') or the original +// input if it can't find the "@" symbol. +func emailUsername(email string) string { + at := strings.Index(email, "@") + if at == -1 { + return email + } else if at == 0 { + return email[1:] + } + return email[:at] +} diff --git a/caddy/letsencrypt/storage_test.go b/caddy/letsencrypt/storage_test.go new file mode 100644 index 000000000..5107c32ab --- /dev/null +++ b/caddy/letsencrypt/storage_test.go @@ -0,0 +1,84 @@ +package letsencrypt + +import ( + "path/filepath" + "testing" +) + +func TestStorage(t *testing.T) { + storage = Storage("./letsencrypt") + + if expected, actual := filepath.Join("letsencrypt", "sites"), storage.Sites(); actual != expected { + t.Errorf("Expected Sites() to return '%s' but got '%s'", expected, actual) + } + if expected, actual := filepath.Join("letsencrypt", "sites", "test.com"), storage.Site("test.com"); actual != expected { + t.Errorf("Expected Site() to return '%s' but got '%s'", expected, actual) + } + if expected, actual := filepath.Join("letsencrypt", "sites", "test.com", "test.com.crt"), storage.SiteCertFile("test.com"); actual != expected { + t.Errorf("Expected SiteCertFile() to return '%s' but got '%s'", expected, actual) + } + if expected, actual := filepath.Join("letsencrypt", "sites", "test.com", "test.com.key"), storage.SiteKeyFile("test.com"); actual != expected { + t.Errorf("Expected SiteKeyFile() to return '%s' but got '%s'", expected, actual) + } + if expected, actual := filepath.Join("letsencrypt", "sites", "test.com", "test.com.json"), storage.SiteMetaFile("test.com"); actual != expected { + t.Errorf("Expected SiteMetaFile() to return '%s' but got '%s'", expected, actual) + } + if expected, actual := filepath.Join("letsencrypt", "users"), storage.Users(); actual != expected { + t.Errorf("Expected Users() to return '%s' but got '%s'", expected, actual) + } + if expected, actual := filepath.Join("letsencrypt", "users", "[email protected]"), storage.User("[email protected]"); actual != expected { + t.Errorf("Expected User() to return '%s' but got '%s'", expected, actual) + } + if expected, actual := filepath.Join("letsencrypt", "users", "[email protected]", "me.json"), storage.UserRegFile("[email protected]"); actual != expected { + t.Errorf("Expected UserRegFile() to return '%s' but got '%s'", expected, actual) + } + if expected, actual := filepath.Join("letsencrypt", "users", "[email protected]", "me.key"), storage.UserKeyFile("[email protected]"); actual != expected { + t.Errorf("Expected UserKeyFile() to return '%s' but got '%s'", expected, actual) + } + + // Test with empty emails + if expected, actual := filepath.Join("letsencrypt", "users", emptyEmail), storage.User(emptyEmail); actual != expected { + t.Errorf("Expected User(\"\") to return '%s' but got '%s'", expected, actual) + } + if expected, actual := filepath.Join("letsencrypt", "users", emptyEmail, emptyEmail+".json"), storage.UserRegFile(""); actual != expected { + t.Errorf("Expected UserRegFile(\"\") to return '%s' but got '%s'", expected, actual) + } + if expected, actual := filepath.Join("letsencrypt", "users", emptyEmail, emptyEmail+".key"), storage.UserKeyFile(""); actual != expected { + t.Errorf("Expected UserKeyFile(\"\") to return '%s' but got '%s'", expected, actual) + } +} + +func TestEmailUsername(t *testing.T) { + for i, test := range []struct { + input, expect string + }{ + { + input: "[email protected]", + expect: "username", + }, + { + input: "[email protected]", + expect: "plus+addressing", + }, + { + input: "[email protected]", + expect: "me+plus-addressing", + }, + { + input: "not-an-email", + expect: "not-an-email", + }, + { + input: "@foobar.com", + expect: "foobar.com", + }, + { + input: emptyEmail, + expect: emptyEmail, + }, + } { + if actual := emailUsername(test.input); actual != test.expect { + t.Errorf("Test %d: Expected username to be '%s' but was '%s'", i, test.expect, actual) + } + } +} diff --git a/caddy/letsencrypt/user.go b/caddy/letsencrypt/user.go new file mode 100644 index 000000000..7fae3bb41 --- /dev/null +++ b/caddy/letsencrypt/user.go @@ -0,0 +1,196 @@ +package letsencrypt + +import ( + "bufio" + "crypto/rand" + "crypto/rsa" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + + "github.com/mholt/caddy/server" + "github.com/xenolf/lego/acme" +) + +// User represents a Let's Encrypt user account. +type User struct { + Email string + Registration *acme.RegistrationResource + key *rsa.PrivateKey +} + +// GetEmail gets u's email. +func (u User) GetEmail() string { + return u.Email +} + +// GetRegistration gets u's registration resource. +func (u User) GetRegistration() *acme.RegistrationResource { + return u.Registration +} + +// GetPrivateKey gets u's private key. +func (u User) GetPrivateKey() *rsa.PrivateKey { + return u.key +} + +// getUser loads the user with the given email from disk. +// If the user does not exist, it will create a new one, +// but it does NOT save new users to the disk or register +// them via ACME. +func getUser(email string) (User, error) { + var user User + + // open user file + regFile, err := os.Open(storage.UserRegFile(email)) + if err != nil { + if os.IsNotExist(err) { + // create a new user + return newUser(email) + } + return user, err + } + defer regFile.Close() + + // load user information + err = json.NewDecoder(regFile).Decode(&user) + if err != nil { + return user, err + } + + // load their private key + user.key, err = loadRSAPrivateKey(storage.UserKeyFile(email)) + if err != nil { + return user, err + } + + return user, nil +} + +// saveUser persists a user's key and account registration +// to the file system. It does NOT register the user via ACME. +func saveUser(user User) error { + // make user account folder + err := os.MkdirAll(storage.User(user.Email), 0700) + if err != nil { + return err + } + + // save private key file + err = saveRSAPrivateKey(user.key, storage.UserKeyFile(user.Email)) + if err != nil { + return err + } + + // save registration file + jsonBytes, err := json.MarshalIndent(&user, "", "\t") + if err != nil { + return err + } + + return ioutil.WriteFile(storage.UserRegFile(user.Email), jsonBytes, 0600) +} + +// newUser creates a new User for the given email address +// with a new private key. This function does NOT save the +// user to disk or register it via ACME. If you want to use +// a user account that might already exist, call getUser +// instead. +func newUser(email string) (User, error) { + user := User{Email: email} + privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse) + if err != nil { + return user, errors.New("error generating private key: " + err.Error()) + } + user.key = privateKey + return user, nil +} + +// getEmail does everything it can to obtain an email +// address from the user to use for TLS for cfg. If it +// cannot get an email address, it returns empty string. +// (It will warn the user of the consequences of an +// empty email.) +func getEmail(cfg server.Config) string { + // First try the tls directive from the Caddyfile + leEmail := cfg.TLS.LetsEncryptEmail + if leEmail == "" { + // Then try memory (command line flag or typed by user previously) + leEmail = DefaultEmail + } + if leEmail == "" { + // Then try to get most recent user email ~/.caddy/users file + userDirs, err := ioutil.ReadDir(storage.Users()) + if err == nil { + var mostRecent os.FileInfo + for _, dir := range userDirs { + if !dir.IsDir() { + continue + } + if mostRecent == nil || dir.ModTime().After(mostRecent.ModTime()) { + mostRecent = dir + } + } + if mostRecent != nil { + leEmail = mostRecent.Name() + } + } + } + if leEmail == "" { + // Alas, we must bother the user and ask for an email address; + // if they proceed they also agree to the SA. + reader := bufio.NewReader(stdin) + fmt.Println("Your sites will be served over HTTPS automatically using Let's Encrypt.") + fmt.Println("By continuing, you agree to the Let's Encrypt Subscriber Agreement at:") + fmt.Println(" " + saURL) // TODO: Show current SA link + fmt.Println("Please enter your email address so you can recover your account if needed.") + fmt.Println("You can leave it blank, but you'll lose the ability to recover your account.") + fmt.Print("Email address: ") + var err error + leEmail, err = reader.ReadString('\n') + if err != nil { + return "" + } + DefaultEmail = leEmail + Agreed = true + } + return strings.TrimSpace(leEmail) +} + +// promptUserAgreement prompts the user to agree to the agreement +// at agreementURL via stdin. If the agreement has changed, then pass +// true as the second argument. If this is the user's first time +// agreeing, pass false. It returns whether the user agreed or not. +func promptUserAgreement(agreementURL string, changed bool) bool { + if changed { + fmt.Printf("The Let's Encrypt Subscriber Agreement has changed:\n %s\n", agreementURL) + fmt.Print("Do you agree to the new terms? (y/n): ") + } else { + fmt.Printf("To continue, you must agree to the Let's Encrypt Subscriber Agreement:\n %s\n", agreementURL) + fmt.Print("Do you agree to the terms? (y/n): ") + } + + reader := bufio.NewReader(stdin) + answer, err := reader.ReadString('\n') + if err != nil { + return false + } + answer = strings.ToLower(strings.TrimSpace(answer)) + + return answer == "y" || answer == "yes" +} + +// stdin is used to read the user's input if prompted; +// this is changed by tests during tests. +var stdin = io.ReadWriter(os.Stdin) + +// The name of the folder for accounts where the email +// address was not provided; default 'username' if you will. +const emptyEmail = "default" + +// TODO: Use latest +const saURL = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf" diff --git a/caddy/letsencrypt/user_test.go b/caddy/letsencrypt/user_test.go new file mode 100644 index 000000000..d074856af --- /dev/null +++ b/caddy/letsencrypt/user_test.go @@ -0,0 +1,192 @@ +package letsencrypt + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/mholt/caddy/server" + "github.com/xenolf/lego/acme" +) + +func TestUser(t *testing.T) { + privateKey, err := rsa.GenerateKey(rand.Reader, 128) + if err != nil { + t.Fatalf("Could not generate test private key: %v", err) + } + u := User{ + Email: "[email protected]", + Registration: new(acme.RegistrationResource), + key: privateKey, + } + + if expected, actual := "[email protected]", u.GetEmail(); actual != expected { + t.Errorf("Expected email '%s' but got '%s'", expected, actual) + } + if u.GetRegistration() == nil { + t.Error("Expected a registration resource, but got nil") + } + if expected, actual := privateKey, u.GetPrivateKey(); actual != expected { + t.Errorf("Expected the private key at address %p but got one at %p instead ", expected, actual) + } +} + +func TestNewUser(t *testing.T) { + email := "[email protected]" + user, err := newUser(email) + if err != nil { + t.Fatalf("Error creating user: %v", err) + } + if user.key == nil { + t.Error("Private key is nil") + } + if user.Email != email { + t.Errorf("Expected email to be %s, but was %s", email, user.Email) + } + if user.Registration != nil { + t.Error("New user already has a registration resource; it shouldn't") + } +} + +func TestSaveUser(t *testing.T) { + storage = Storage("./testdata") + defer os.RemoveAll(string(storage)) + + email := "[email protected]" + user, err := newUser(email) + if err != nil { + t.Fatalf("Error creating user: %v", err) + } + + err = saveUser(user) + if err != nil { + t.Fatalf("Error saving user: %v", err) + } + _, err = os.Stat(storage.UserRegFile(email)) + if err != nil { + t.Errorf("Cannot access user registration file, error: %v", err) + } + _, err = os.Stat(storage.UserKeyFile(email)) + if err != nil { + t.Errorf("Cannot access user private key file, error: %v", err) + } +} + +func TestGetUserDoesNotAlreadyExist(t *testing.T) { + storage = Storage("./testdata") + defer os.RemoveAll(string(storage)) + + user, err := getUser("[email protected]") + if err != nil { + t.Fatalf("Error getting user: %v", err) + } + + if user.key == nil { + t.Error("Expected user to have a private key, but it was nil") + } +} + +func TestGetUserAlreadyExists(t *testing.T) { + storage = Storage("./testdata") + defer os.RemoveAll(string(storage)) + + email := "[email protected]" + + // Set up test + user, err := newUser(email) + if err != nil { + t.Fatalf("Error creating user: %v", err) + } + err = saveUser(user) + if err != nil { + t.Fatalf("Error saving user: %v", err) + } + + // Expect to load user from disk + user2, err := getUser(email) + if err != nil { + t.Fatalf("Error getting user: %v", err) + } + + // Assert keys are the same + if !rsaPrivateKeysSame(user.key, user2.key) { + t.Error("Expected private key to be the same after loading, but it wasn't") + } + + // Assert emails are the same + if user.Email != user2.Email { + t.Errorf("Expected emails to be equal, but was '%s' before and '%s' after loading", user.Email, user2.Email) + } +} + +func TestGetEmail(t *testing.T) { + storage = Storage("./testdata") + defer os.RemoveAll(string(storage)) + DefaultEmail = "[email protected]" + + // Test1: Use email in config + config := server.Config{ + TLS: server.TLSConfig{ + LetsEncryptEmail: "[email protected]", + }, + } + actual := getEmail(config) + if actual != "[email protected]" { + t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", "[email protected]", actual) + } + + // Test2: Use default email from flag (or user previously typing it) + actual = getEmail(server.Config{}) + if actual != DefaultEmail { + t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", DefaultEmail, actual) + } + + // Test3: Get input from user + DefaultEmail = "" + stdin = new(bytes.Buffer) + _, err := io.Copy(stdin, strings.NewReader("[email protected]\n")) + if err != nil { + t.Fatalf("Could not simulate user input, error: %v", err) + } + actual = getEmail(server.Config{}) + if actual != "[email protected]" { + t.Errorf("Did not get correct email from user input prompt; expected '%s' but got '%s'", "[email protected]", actual) + } + + // Test4: Get most recent email from before + DefaultEmail = "" + for i, eml := range []string{ + "[email protected]", + "[email protected]", + "[email protected]", + } { + u, err := newUser(eml) + if err != nil { + t.Fatalf("Error creating user %d: %v", i, err) + } + err = saveUser(u) + if err != nil { + t.Fatalf("Error saving user %d: %v", i, err) + } + + // Change modified time so they're all different, so the test becomes deterministic + f, err := os.Stat(storage.User(eml)) + if err != nil { + t.Fatalf("Could not access user folder for '%s': %v", eml, err) + } + chTime := f.ModTime().Add(-(time.Duration(i) * time.Second)) + if err := os.Chtimes(storage.User(eml), chTime, chTime); err != nil { + t.Fatalf("Could not change user folder mod time for '%s': %v", eml, err) + } + } + + actual = getEmail(server.Config{}) + if actual != "[email protected]" { + t.Errorf("Did not get correct email from storage; expected '%s' but got '%s'", "[email protected]", actual) + } +} diff --git a/config/parse/dispenser.go b/caddy/parse/dispenser.go index 08aa6e76d..08aa6e76d 100644 --- a/config/parse/dispenser.go +++ b/caddy/parse/dispenser.go diff --git a/config/parse/dispenser_test.go b/caddy/parse/dispenser_test.go index 20a7ddcac..20a7ddcac 100644 --- a/config/parse/dispenser_test.go +++ b/caddy/parse/dispenser_test.go diff --git a/config/parse/import_test1.txt b/caddy/parse/import_test1.txt index dac7b29be..dac7b29be 100644 --- a/config/parse/import_test1.txt +++ b/caddy/parse/import_test1.txt diff --git a/config/parse/import_test2.txt b/caddy/parse/import_test2.txt index 140c87939..140c87939 100644 --- a/config/parse/import_test2.txt +++ b/caddy/parse/import_test2.txt diff --git a/config/parse/lexer.go b/caddy/parse/lexer.go index d2939eba2..d2939eba2 100644 --- a/config/parse/lexer.go +++ b/caddy/parse/lexer.go diff --git a/config/parse/lexer_test.go b/caddy/parse/lexer_test.go index f12c7e7dc..f12c7e7dc 100644 --- a/config/parse/lexer_test.go +++ b/caddy/parse/lexer_test.go diff --git a/config/parse/parse.go b/caddy/parse/parse.go index b44041d4f..84043e606 100644 --- a/config/parse/parse.go +++ b/caddy/parse/parse.go @@ -5,9 +5,11 @@ import "io" // ServerBlocks parses the input just enough to organize tokens, // in order, by server block. No further parsing is performed. -// Server blocks are returned in the order in which they appear. -func ServerBlocks(filename string, input io.Reader) ([]serverBlock, error) { - p := parser{Dispenser: NewDispenser(filename, input)} +// If checkDirectives is true, only valid directives will be allowed +// otherwise we consider it a parse error. Server blocks are returned +// in the order in which they appear. +func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]serverBlock, error) { + p := parser{Dispenser: NewDispenser(filename, input), checkDirectives: checkDirectives} blocks, err := p.parseAll() return blocks, err } diff --git a/config/parse/parse_test.go b/caddy/parse/parse_test.go index 48746300f..48746300f 100644 --- a/config/parse/parse_test.go +++ b/caddy/parse/parse_test.go diff --git a/config/parse/parsing.go b/caddy/parse/parsing.go index 594553913..b24b46abb 100644 --- a/config/parse/parsing.go +++ b/caddy/parse/parsing.go @@ -9,8 +9,9 @@ import ( type parser struct { Dispenser - block serverBlock // current server block being parsed - eof bool // if we encounter a valid EOF in a hard place + block serverBlock // current server block being parsed + eof bool // if we encounter a valid EOF in a hard place + checkDirectives bool // if true, directives must be known } func (p *parser) parseAll() ([]serverBlock, error) { @@ -220,8 +221,10 @@ func (p *parser) directive() error { dir := p.Val() nesting := 0 - if _, ok := ValidDirectives[dir]; !ok { - return p.Errf("Unknown directive '%s'", dir) + if p.checkDirectives { + if _, ok := ValidDirectives[dir]; !ok { + return p.Errf("Unknown directive '%s'", dir) + } } // The directive itself is appended as a relevant token diff --git a/config/parse/parsing_test.go b/caddy/parse/parsing_test.go index c8a7ef0be..afd5870f3 100644 --- a/config/parse/parsing_test.go +++ b/caddy/parse/parsing_test.go @@ -375,6 +375,6 @@ func setupParseTests() { func testParser(input string) parser { buf := strings.NewReader(input) - p := parser{Dispenser: NewDispenser("Test", buf)} + p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true} return p } diff --git a/caddy/restart.go b/caddy/restart.go new file mode 100644 index 000000000..eae8604ff --- /dev/null +++ b/caddy/restart.go @@ -0,0 +1,102 @@ +// +build !windows + +package caddy + +import ( + "encoding/gob" + "io/ioutil" + "log" + "os" + "syscall" +) + +func init() { + gob.Register(CaddyfileInput{}) +} + +// Restart restarts the entire application; gracefully with zero +// downtime if on a POSIX-compatible system, or forcefully if on +// Windows but with imperceptibly-short downtime. +// +// The restarted application will use newCaddyfile as its input +// configuration. If newCaddyfile is nil, the current (existing) +// Caddyfile configuration will be used. +// +// Note: The process must exist in the same place on the disk in +// order for this to work. Thus, multiple graceful restarts don't +// work if executing with `go run`, since the binary is cleaned up +// when `go run` sees the initial parent process exit. +func Restart(newCaddyfile Input) error { + if newCaddyfile == nil { + caddyfileMu.Lock() + newCaddyfile = caddyfile + caddyfileMu.Unlock() + } + + if len(os.Args) == 0 { // this should never happen... + os.Args = []string{""} + } + + // Tell the child that it's a restart + os.Setenv("CADDY_RESTART", "true") + + // Prepare our payload to the child process + cdyfileGob := caddyfileGob{ + ListenerFds: make(map[string]uintptr), + Caddyfile: newCaddyfile, + } + + // Prepare a pipe to the fork's stdin so it can get the Caddyfile + rpipe, wpipe, err := os.Pipe() + if err != nil { + return err + } + + // Prepare a pipe that the child process will use to communicate + // its success or failure with us, the parent + sigrpipe, sigwpipe, err := os.Pipe() + if err != nil { + return err + } + + // Pass along current environment and file descriptors to child. + // Ordering here is very important: stdin, stdout, stderr, sigpipe, + // and then the listener file descriptors (in order). + fds := []uintptr{rpipe.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), sigwpipe.Fd()} + + // Now add file descriptors of the sockets + serversMu.Lock() + for i, s := range servers { + fds = append(fds, s.ListenerFd()) + cdyfileGob.ListenerFds[s.Addr] = uintptr(4 + i) // 4 fds come before any of the listeners + } + serversMu.Unlock() + + // Fork the process with the current environment and file descriptors + execSpec := &syscall.ProcAttr{ + Env: os.Environ(), + Files: fds, + } + _, err = syscall.ForkExec(os.Args[0], os.Args, execSpec) + if err != nil { + return err + } + + // Feed it the Caddyfile + err = gob.NewEncoder(wpipe).Encode(cdyfileGob) + if err != nil { + return err + } + wpipe.Close() + + // Wait for child process to signal success or fail + sigwpipe.Close() // close our copy of the write end of the pipe or we might be stuck + answer, err := ioutil.ReadAll(sigrpipe) + if err != nil || len(answer) == 0 { + log.Println("restart: child failed to initialize; changes not applied") + return incompleteRestartErr + } + + // Child process is listening now; we can stop all our servers here. + return Stop() +} diff --git a/caddy/restart_windows.go b/caddy/restart_windows.go new file mode 100644 index 000000000..00ec94a71 --- /dev/null +++ b/caddy/restart_windows.go @@ -0,0 +1,25 @@ +package caddy + +func Restart(newCaddyfile Input) error { + if newCaddyfile == nil { + caddyfileMu.Lock() + newCaddyfile = caddyfile + caddyfileMu.Unlock() + } + + wg.Add(1) // barrier so Wait() doesn't unblock + + err := Stop() + if err != nil { + return err + } + + err = Start(newCaddyfile) + if err != nil { + return err + } + + wg.Done() // take down our barrier + + return nil +} diff --git a/config/setup/basicauth.go b/caddy/setup/basicauth.go index bc57d1c6e..bc57d1c6e 100644 --- a/config/setup/basicauth.go +++ b/caddy/setup/basicauth.go diff --git a/config/setup/basicauth_test.go b/caddy/setup/basicauth_test.go index a94d6e695..a94d6e695 100644 --- a/config/setup/basicauth_test.go +++ b/caddy/setup/basicauth_test.go diff --git a/config/setup/bindhost.go b/caddy/setup/bindhost.go index 3e4bf89b3..3e4bf89b3 100644 --- a/config/setup/bindhost.go +++ b/caddy/setup/bindhost.go diff --git a/config/setup/browse.go b/caddy/setup/browse.go index cef37c057..cef37c057 100644 --- a/config/setup/browse.go +++ b/caddy/setup/browse.go diff --git a/config/setup/controller.go b/caddy/setup/controller.go index 3890ab447..e31207263 100644 --- a/config/setup/controller.go +++ b/caddy/setup/controller.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "github.com/mholt/caddy/config/parse" + "github.com/mholt/caddy/caddy/parse" "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/server" ) diff --git a/config/setup/errors.go b/caddy/setup/errors.go index 5aa09b9ff..5aa09b9ff 100644 --- a/config/setup/errors.go +++ b/caddy/setup/errors.go diff --git a/config/setup/errors_test.go b/caddy/setup/errors_test.go index 216c5de85..216c5de85 100644 --- a/config/setup/errors_test.go +++ b/caddy/setup/errors_test.go diff --git a/config/setup/ext.go b/caddy/setup/ext.go index 4495da664..4495da664 100644 --- a/config/setup/ext.go +++ b/caddy/setup/ext.go diff --git a/config/setup/ext_test.go b/caddy/setup/ext_test.go index 24e3cf947..24e3cf947 100644 --- a/config/setup/ext_test.go +++ b/caddy/setup/ext_test.go diff --git a/config/setup/fastcgi.go b/caddy/setup/fastcgi.go index a2a7e8794..a2a7e8794 100644 --- a/config/setup/fastcgi.go +++ b/caddy/setup/fastcgi.go diff --git a/config/setup/fastcgi_test.go b/caddy/setup/fastcgi_test.go index 661b92e50..661b92e50 100644 --- a/config/setup/fastcgi_test.go +++ b/caddy/setup/fastcgi_test.go diff --git a/config/setup/gzip.go b/caddy/setup/gzip.go index a81c5f170..a81c5f170 100644 --- a/config/setup/gzip.go +++ b/caddy/setup/gzip.go diff --git a/config/setup/gzip_test.go b/caddy/setup/gzip_test.go index 22d01d7a1..22d01d7a1 100644 --- a/config/setup/gzip_test.go +++ b/caddy/setup/gzip_test.go diff --git a/config/setup/headers.go b/caddy/setup/headers.go index 553f20b18..553f20b18 100644 --- a/config/setup/headers.go +++ b/caddy/setup/headers.go diff --git a/config/setup/headers_test.go b/caddy/setup/headers_test.go index 412e76642..412e76642 100644 --- a/config/setup/headers_test.go +++ b/caddy/setup/headers_test.go diff --git a/config/setup/internal.go b/caddy/setup/internal.go index e83863b80..e83863b80 100644 --- a/config/setup/internal.go +++ b/caddy/setup/internal.go diff --git a/config/setup/internal_test.go b/caddy/setup/internal_test.go index f4d0ed8b9..f4d0ed8b9 100644 --- a/config/setup/internal_test.go +++ b/caddy/setup/internal_test.go diff --git a/config/setup/log.go b/caddy/setup/log.go index 8bb4788a1..8bb4788a1 100644 --- a/config/setup/log.go +++ b/caddy/setup/log.go diff --git a/config/setup/log_test.go b/caddy/setup/log_test.go index ae7a96e31..ae7a96e31 100644 --- a/config/setup/log_test.go +++ b/caddy/setup/log_test.go diff --git a/config/setup/markdown.go b/caddy/setup/markdown.go index 65344a741..65344a741 100644 --- a/config/setup/markdown.go +++ b/caddy/setup/markdown.go diff --git a/config/setup/markdown_test.go b/caddy/setup/markdown_test.go index 5bf012b08..5bf012b08 100644 --- a/config/setup/markdown_test.go +++ b/caddy/setup/markdown_test.go diff --git a/config/setup/mime.go b/caddy/setup/mime.go index 760056eba..760056eba 100644 --- a/config/setup/mime.go +++ b/caddy/setup/mime.go diff --git a/config/setup/mime_test.go b/caddy/setup/mime_test.go index 7f8d8de68..7f8d8de68 100644 --- a/config/setup/mime_test.go +++ b/caddy/setup/mime_test.go diff --git a/config/setup/proxy.go b/caddy/setup/proxy.go index 3011cb0e4..3011cb0e4 100644 --- a/config/setup/proxy.go +++ b/caddy/setup/proxy.go diff --git a/config/setup/redir.go b/caddy/setup/redir.go index 63488f4ab..63488f4ab 100644 --- a/config/setup/redir.go +++ b/caddy/setup/redir.go diff --git a/config/setup/rewrite.go b/caddy/setup/rewrite.go index b510a237b..b510a237b 100644 --- a/config/setup/rewrite.go +++ b/caddy/setup/rewrite.go diff --git a/config/setup/rewrite_test.go b/caddy/setup/rewrite_test.go index f54266788..f54266788 100644 --- a/config/setup/rewrite_test.go +++ b/caddy/setup/rewrite_test.go diff --git a/config/setup/roller.go b/caddy/setup/roller.go index fedc52c58..fedc52c58 100644 --- a/config/setup/roller.go +++ b/caddy/setup/roller.go diff --git a/config/setup/root.go b/caddy/setup/root.go index 892578b7a..892578b7a 100644 --- a/config/setup/root.go +++ b/caddy/setup/root.go diff --git a/config/setup/root_test.go b/caddy/setup/root_test.go index 8b38e6d04..8b38e6d04 100644 --- a/config/setup/root_test.go +++ b/caddy/setup/root_test.go diff --git a/config/setup/startupshutdown.go b/caddy/setup/startupshutdown.go index e4d873056..e4d873056 100644 --- a/config/setup/startupshutdown.go +++ b/caddy/setup/startupshutdown.go diff --git a/config/setup/startupshutdown_test.go b/caddy/setup/startupshutdown_test.go index cf07a7e8c..cf07a7e8c 100644 --- a/config/setup/startupshutdown_test.go +++ b/caddy/setup/startupshutdown_test.go diff --git a/config/setup/templates.go b/caddy/setup/templates.go index f8d7e98bd..f8d7e98bd 100644 --- a/config/setup/templates.go +++ b/caddy/setup/templates.go diff --git a/config/setup/templates_test.go b/caddy/setup/templates_test.go index b1cfb29ce..b1cfb29ce 100644 --- a/config/setup/templates_test.go +++ b/caddy/setup/templates_test.go diff --git a/config/setup/testdata/blog/first_post.md b/caddy/setup/testdata/blog/first_post.md index f26583b75..f26583b75 100644 --- a/config/setup/testdata/blog/first_post.md +++ b/caddy/setup/testdata/blog/first_post.md diff --git a/config/setup/testdata/header.html b/caddy/setup/testdata/header.html index 9c96e0e37..9c96e0e37 100644 --- a/config/setup/testdata/header.html +++ b/caddy/setup/testdata/header.html diff --git a/config/setup/testdata/tpl_with_include.html b/caddy/setup/testdata/tpl_with_include.html index 95eeae0c8..95eeae0c8 100644 --- a/config/setup/testdata/tpl_with_include.html +++ b/caddy/setup/testdata/tpl_with_include.html diff --git a/config/setup/tls.go b/caddy/setup/tls.go index e9d3db7eb..7519202b5 100644 --- a/config/setup/tls.go +++ b/caddy/setup/tls.go @@ -9,24 +9,37 @@ import ( ) func TLS(c *Controller) (middleware.Middleware, error) { - c.TLS.Enabled = true - if c.Port == "http" { c.TLS.Enabled = false log.Printf("Warning: TLS disabled for %s://%s. To force TLS over the plaintext HTTP port, "+ "specify port 80 explicitly (https://%s:80).", c.Port, c.Host, c.Host) + } else { + c.TLS.Enabled = true // they had a tls directive, so assume it's on unless we confirm otherwise later } for c.Next() { - if !c.NextArg() { - return nil, c.ArgErr() - } - c.TLS.Certificate = c.Val() + args := c.RemainingArgs() + switch len(args) { + case 1: + c.TLS.LetsEncryptEmail = args[0] - if !c.NextArg() { + // user can force-disable LE activation this way + if c.TLS.LetsEncryptEmail == "off" { + c.TLS.Enabled = false + } + case 2: + c.TLS.Certificate = args[0] + c.TLS.Key = args[1] + + // manual HTTPS configuration without port specified should be + // served on the HTTPS port; that is what user would expect, and + // makes it consistent with how the letsencrypt package works. + if c.Port == "" { + c.Port = "https" + } + default: return nil, c.ArgErr() } - c.TLS.Key = c.Val() // Optional block for c.NextBlock() { diff --git a/config/setup/tls_test.go b/caddy/setup/tls_test.go index fe1ce6559..fdea1e0c7 100644 --- a/config/setup/tls_test.go +++ b/caddy/setup/tls_test.go @@ -70,14 +70,7 @@ func TestTLSParseIncompleteParams(t *testing.T) { _, err := TLS(c) if err == nil { - t.Errorf("Expected errors, but no error returned") - } - - c = NewTestController(`tls cert.key`) - - _, err = TLS(c) - if err == nil { - t.Errorf("Expected errors, but no error returned") + t.Errorf("Expected errors (first check), but no error returned") } } diff --git a/config/setup/websocket.go b/caddy/setup/websocket.go index 33df76d7e..33df76d7e 100644 --- a/config/setup/websocket.go +++ b/caddy/setup/websocket.go diff --git a/config/setup/websocket_test.go b/caddy/setup/websocket_test.go index ae3513602..ae3513602 100644 --- a/config/setup/websocket_test.go +++ b/caddy/setup/websocket_test.go diff --git a/caddy/sigtrap.go b/caddy/sigtrap.go new file mode 100644 index 000000000..b9cbec6a6 --- /dev/null +++ b/caddy/sigtrap.go @@ -0,0 +1,33 @@ +package caddy + +import ( + "log" + "os" + "os/signal" + + "github.com/mholt/caddy/server" +) + +func init() { + // Trap quit signals (cross-platform) + go func() { + shutdown := make(chan os.Signal, 1) + signal.Notify(shutdown, os.Interrupt, os.Kill) + <-shutdown + + var exitCode int + + serversMu.Lock() + errs := server.ShutdownCallbacks(servers) + serversMu.Unlock() + + if len(errs) > 0 { + for _, err := range errs { + log.Println(err) + } + exitCode = 1 + } + + os.Exit(exitCode) + }() +} diff --git a/caddy/sigtrap_posix.go b/caddy/sigtrap_posix.go new file mode 100644 index 000000000..521866fd1 --- /dev/null +++ b/caddy/sigtrap_posix.go @@ -0,0 +1,43 @@ +// +build !windows + +package caddy + +import ( + "io/ioutil" + "log" + "os" + "os/signal" + "syscall" +) + +func init() { + // Trap POSIX-only signals + go func() { + reload := make(chan os.Signal, 1) + signal.Notify(reload, syscall.SIGUSR1) // reload configuration + + for { + <-reload + + var updatedCaddyfile Input + + caddyfileMu.Lock() + if caddyfile.IsFile() { + body, err := ioutil.ReadFile(caddyfile.Path()) + if err == nil { + caddyfile = CaddyfileInput{ + Filepath: caddyfile.Path(), + Contents: body, + RealFile: true, + } + } + } + caddyfileMu.Unlock() + + err := Restart(updatedCaddyfile) + if err != nil { + log.Println("error at restart:", err) + } + } + }() +} diff --git a/dist/CHANGES.txt b/dist/CHANGES.txt index 6a2c201c4..623c5cba2 100644 --- a/dist/CHANGES.txt +++ b/dist/CHANGES.txt @@ -1,7 +1,24 @@ CHANGES -<master> +0.8 beta +- Let's Encrypt (free, automatic, fully-managed HTTPS for your sites) +- Graceful restarts (for POSIX-compatible systems) +- Major internal refactoring to allow use of Caddy as library - New directive 'mime' to customize Content-Type based on file extension +- New -accept flag to accept Let's Encrypt SA without prompt +- New -email flag to customize default email used for ACME transactions +- New -ca flag to customize ACME CA server URL +- New -revoke flag to revoke a certificate +- browse: Render filenames with multiple whitespace properly +- markdown: Include Last-Modified header in response +- startup, shutdown: Better Windows support +- templates: Bug fix for .Host when port is absent +- templates: Include Last-Modified header in response +- templates: Support for custom delimiters +- tls: For non-local hosts, default port is now 443 unless specified +- tls: Force-disable HTTPS +- tls: Specify Let's Encrypt email address +- Many, many more tests and numerous bug fixes and improvements 0.7.6 (September 28, 2015) diff --git a/dist/README.txt b/dist/README.txt index de3fde6a4..047e1955c 100644 --- a/dist/README.txt +++ b/dist/README.txt @@ -1,4 +1,4 @@ -CADDY 0.7.6 +CADDY 0.8 beta Website https://caddyserver.com @@ -1,174 +1,160 @@ package main import ( - "bytes" + "errors" "flag" "fmt" "io/ioutil" "log" "os" - "os/exec" - "path" "runtime" "strconv" "strings" - "github.com/mholt/caddy/app" - "github.com/mholt/caddy/config" - "github.com/mholt/caddy/server" + "github.com/mholt/caddy/caddy" + "github.com/mholt/caddy/caddy/letsencrypt" ) var ( conf string cpu string version bool + revoke string +) + +const ( + appName = "Caddy" + appVersion = "0.8 beta" ) func init() { - flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+config.DefaultConfigFile+")") - flag.BoolVar(&app.HTTP2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib - flag.BoolVar(&app.Quiet, "quiet", false, "Quiet mode (no initialization output)") + flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")") + flag.BoolVar(&caddy.HTTP2, "http2", true, "HTTP/2 support") // TODO: temporary flag until http2 merged into std lib + flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)") flag.StringVar(&cpu, "cpu", "100%", "CPU cap") - flag.StringVar(&config.Root, "root", config.DefaultRoot, "Root path to default site") - flag.StringVar(&config.Host, "host", config.DefaultHost, "Default host") - flag.StringVar(&config.Port, "port", config.DefaultPort, "Default port") + flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site") + flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host") + flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port") flag.BoolVar(&version, "version", false, "Show version") + // TODO: Boulder dev URL is: http://192.168.99.100:4000 + // TODO: Staging API URL is: https://acme-staging.api.letsencrypt.org + // TODO: Production endpoint is: https://acme-v01.api.letsencrypt.org + flag.StringVar(&letsencrypt.CAUrl, "ca", "https://acme-staging.api.letsencrypt.org", "Certificate authority ACME server") + flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement") + flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default Let's Encrypt account email address") + flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") } func main() { flag.Parse() + caddy.AppName = appName + caddy.AppVersion = appVersion + if version { - fmt.Printf("%s %s\n", app.Name, app.Version) + fmt.Printf("%s %s\n", caddy.AppName, caddy.AppVersion) + os.Exit(0) + } + if revoke != "" { + err := letsencrypt.Revoke(revoke) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Revoked certificate for %s\n", revoke) os.Exit(0) } // Set CPU cap - err := app.SetCPU(cpu) + err := setCPU(cpu) if err != nil { log.Fatal(err) } - // Load config from file - addresses, err := loadConfigs() + // Get Caddyfile input + caddyfile, err := caddy.LoadCaddyfile(loadCaddyfile) if err != nil { log.Fatal(err) } - // Start each server with its one or more configurations - for addr, configs := range addresses { - s, err := server.New(addr.String(), configs) - if err != nil { - log.Fatal(err) - } - s.HTTP2 = app.HTTP2 // TODO: This setting is temporary - app.Wg.Add(1) - go func(s *server.Server) { - defer app.Wg.Done() - err := s.Serve() - if err != nil { - log.Fatal(err) // kill whole process to avoid a half-alive zombie server - } - }(s) - - app.Servers = append(app.Servers, s) - } - - // Show initialization output - if !app.Quiet { - var checkedFdLimit bool - for addr, configs := range addresses { - for _, conf := range configs { - // Print address of site - fmt.Println(conf.Address()) - - // Note if non-localhost site resolves to loopback interface - if addr.IP.IsLoopback() && !isLocalhost(conf.Host) { - fmt.Printf("Notice: %s is only accessible on this machine (%s)\n", - conf.Host, addr.IP.String()) - } - if !checkedFdLimit && !addr.IP.IsLoopback() && !isLocalhost(conf.Host) { - checkFdlimit() - checkedFdLimit = true - } - } - } - } - - // Wait for all listeners to stop - app.Wg.Wait() -} - -// checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum. -func checkFdlimit() { - const min = 4096 - - // Warn if ulimit is too low for production sites - if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { - out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH - if err == nil { - // Note that an error here need not be reported - lim, err := strconv.Atoi(string(bytes.TrimSpace(out))) - if err == nil && lim < min { - fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min) - } - } + // Start your engines + err = caddy.Start(caddyfile) + if err != nil { + log.Fatal(err) } -} -// isLocalhost returns true if the string looks explicitly like a localhost address. -func isLocalhost(s string) bool { - return s == "localhost" || s == "::1" || strings.HasPrefix(s, "127.") + // Twiddle your thumbs + caddy.Wait() } -// loadConfigs loads configuration from a file or stdin (piped). -// The configurations are grouped by bind address. -// Configuration is obtained from one of four sources, tried -// in this order: 1. -conf flag, 2. stdin, 3. command line argument 4. Caddyfile. -// If none of those are available, a default configuration is loaded. -func loadConfigs() (config.Group, error) { +func loadCaddyfile() (caddy.Input, error) { // -conf flag if conf != "" { - file, err := os.Open(conf) + contents, err := ioutil.ReadFile(conf) if err != nil { return nil, err } - defer file.Close() - return config.Load(path.Base(conf), file) + return caddy.CaddyfileInput{ + Contents: contents, + Filepath: conf, + RealFile: true, + }, nil } - // stdin - fi, err := os.Stdin.Stat() - if err == nil && fi.Mode()&os.ModeCharDevice == 0 { - // Note that a non-nil error is not a problem. Windows - // will not create a stdin if there is no pipe, which - // produces an error when calling Stat(). But Unix will - // make one either way, which is why we also check that - // bitmask. - confBody, err := ioutil.ReadAll(os.Stdin) - if err != nil { - log.Fatal(err) - } - if len(confBody) > 0 { - return config.Load("stdin", bytes.NewReader(confBody)) - } - } - - // Command line Arg + // command line args if flag.NArg() > 0 { - confBody := ":" + config.DefaultPort + "\n" + strings.Join(flag.Args(), "\n") - return config.Load("args", bytes.NewBufferString(confBody)) + confBody := ":" + caddy.DefaultPort + "\n" + strings.Join(flag.Args(), "\n") + return caddy.CaddyfileInput{ + Contents: []byte(confBody), + Filepath: "args", + }, nil } - // Caddyfile - file, err := os.Open(config.DefaultConfigFile) + // Caddyfile in cwd + contents, err := ioutil.ReadFile(caddy.DefaultConfigFile) if err != nil { if os.IsNotExist(err) { - return config.Default() + return caddy.DefaultInput, nil } return nil, err } - defer file.Close() + return caddy.CaddyfileInput{ + Contents: contents, + Filepath: caddy.DefaultConfigFile, + RealFile: true, + }, nil +} + +// setCPU parses string cpu and sets GOMAXPROCS +// according to its value. It accepts either +// a number (e.g. 3) or a percent (e.g. 50%). +func setCPU(cpu string) error { + var numCPU int + + availCPU := runtime.NumCPU() + + if strings.HasSuffix(cpu, "%") { + // Percent + var percent float32 + pctStr := cpu[:len(cpu)-1] + pctInt, err := strconv.Atoi(pctStr) + if err != nil || pctInt < 1 || pctInt > 100 { + return errors.New("invalid CPU value: percentage must be between 1-100") + } + percent = float32(pctInt) / 100 + numCPU = int(float32(availCPU) * percent) + } else { + // Number + num, err := strconv.Atoi(cpu) + if err != nil || num < 1 { + return errors.New("invalid CPU value: provide a number or percent greater than 0") + } + numCPU = num + } + + if numCPU > availCPU { + numCPU = availCPU + } - return config.Load(config.DefaultConfigFile, file) + runtime.GOMAXPROCS(numCPU) + return nil } diff --git a/app/app_test.go b/main_test.go index e9dc640a0..4e61afa81 100644 --- a/app/app_test.go +++ b/main_test.go @@ -1,10 +1,8 @@ -package app_test +package main import ( "runtime" "testing" - - "github.com/mholt/caddy/app" ) func TestSetCPU(t *testing.T) { @@ -26,7 +24,7 @@ func TestSetCPU(t *testing.T) { {"invalid input%", currentCPU, true}, {"9999", maxCPU, false}, // over available CPU } { - err := app.SetCPU(test.input) + err := setCPU(test.input) if test.shouldErr && err == nil { t.Errorf("Test %d: Expected error, but there wasn't any", i) } diff --git a/middleware/proxy/upstream.go b/middleware/proxy/upstream.go index 3ab8aa9b8..f068907ef 100644 --- a/middleware/proxy/upstream.go +++ b/middleware/proxy/upstream.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/mholt/caddy/config/parse" + "github.com/mholt/caddy/caddy/parse" ) var ( diff --git a/server/config.go b/server/config.go index 4bcea8b22..a3bb5f50d 100644 --- a/server/config.go +++ b/server/config.go @@ -50,13 +50,13 @@ func (c Config) Address() string { return net.JoinHostPort(c.Host, c.Port) } -// TLSConfig describes how TLS should be configured and used, -// if at all. A certificate and key are both required. -// The rest is optional. +// TLSConfig describes how TLS should be configured and used. type TLSConfig struct { Enabled bool Certificate string Key string + LetsEncryptEmail string + OCSPStaple []byte Ciphers []uint16 ProtocolMinVersion uint16 ProtocolMaxVersion uint16 diff --git a/server/graceful.go b/server/graceful.go new file mode 100644 index 000000000..6b2ae4f5c --- /dev/null +++ b/server/graceful.go @@ -0,0 +1,76 @@ +package server + +import ( + "net" + "os" + "sync" + "syscall" +) + +// newGracefulListener returns a gracefulListener that wraps l and +// uses wg (stored in the host server) to count connections. +func newGracefulListener(l ListenerFile, wg *sync.WaitGroup) *gracefulListener { + gl := &gracefulListener{ListenerFile: l, stop: make(chan error), httpWg: wg} + go func() { + <-gl.stop + gl.stopped = true + gl.stop <- gl.ListenerFile.Close() + }() + return gl +} + +// gracefuListener is a net.Listener which can +// count the number of connections on it. Its +// methods mainly wrap net.Listener to be graceful. +type gracefulListener struct { + ListenerFile + stop chan error + stopped bool + httpWg *sync.WaitGroup // pointer to the host's wg used for counting connections +} + +// Accept accepts a connection. This type wraps +func (gl *gracefulListener) Accept() (c net.Conn, err error) { + c, err = gl.ListenerFile.Accept() + if err != nil { + return + } + c = gracefulConn{Conn: c, httpWg: gl.httpWg} + gl.httpWg.Add(1) + return +} + +// Close immediately closes the listener. +func (gl *gracefulListener) Close() error { + if gl.stopped { + return syscall.EINVAL + } + gl.stop <- nil + return <-gl.stop +} + +// File implements ListenerFile; it gets the file of the listening socket. +func (gl *gracefulListener) File() (*os.File, error) { + return gl.ListenerFile.File() +} + +// gracefulConn represents a connection on a +// gracefulListener so that we can keep track +// of the number of connections, thus facilitating +// a graceful shutdown. +type gracefulConn struct { + net.Conn + httpWg *sync.WaitGroup // pointer to the host server's connection waitgroup +} + +// Close closes c's underlying connection while updating the wg count. +func (c gracefulConn) Close() error { + err := c.Conn.Close() + if err != nil { + return err + } + // close can fail on http2 connections (as of Oct. 2015, before http2 in std lib) + // so don't decrement count unless close succeeds + c.httpWg.Done() + return nil +} diff --git a/server/server.go b/server/server.go index 24aa92eb5..15128996f 100644 --- a/server/server.go +++ b/server/server.go @@ -12,23 +12,42 @@ import ( "net" "net/http" "os" - "os/signal" + "runtime" + "sync" + "time" "golang.org/x/net/http2" ) // Server represents an instance of a server, which serves -// static content at a particular address (host and port). +// HTTP requests at a particular address (host and port). A +// server is capable of serving numerous virtual hosts on +// the same address and the listener may be stopped for +// graceful termination (POSIX only). type Server struct { - HTTP2 bool // temporary while http2 is not in std lib (TODO: remove flag when part of std lib) - address string // the actual address for net.Listen to listen on - tls bool // whether this server is serving all HTTPS hosts or not - vhosts map[string]virtualHost // virtual hosts keyed by their address + *http.Server + HTTP2 bool // temporary while http2 is not in std lib (TODO: remove flag when part of std lib) + tls bool // whether this server is serving all HTTPS hosts or not + vhosts map[string]virtualHost // virtual hosts keyed by their address + listener ListenerFile // the listener which is bound to the socket + listenerMu sync.Mutex // protects listener + httpWg sync.WaitGroup // used to wait on outstanding connections + startChan chan struct{} // used to block until server is finished starting +} + +type ListenerFile interface { + net.Listener + File() (*os.File, error) } // New creates a new Server which will bind to addr and serve // the sites/hosts configured in configs. This function does // not start serving. +// +// Do not re-use a server (start, stop, then start again). We +// could probably add more locking to make this possible, but +// as it stands, you should dispose of a server after stopping it. +// The behavior of serving with a spent server is undefined. func New(addr string, configs []Config) (*Server, error) { var tls bool if len(configs) > 0 { @@ -36,14 +55,31 @@ func New(addr string, configs []Config) (*Server, error) { } s := &Server{ - address: addr, - tls: tls, - vhosts: make(map[string]virtualHost), + Server: &http.Server{ + Addr: addr, + // TODO: Make these values configurable? + // ReadTimeout: 2 * time.Minute, + // WriteTimeout: 2 * time.Minute, + // MaxHeaderBytes: 1 << 16, + }, + tls: tls, + vhosts: make(map[string]virtualHost), + startChan: make(chan struct{}), } + s.Handler = s // this is weird, but whatever + + // We have to bound our wg with one increment + // to prevent a "race condition" that is hard-coded + // into sync.WaitGroup.Wait() - basically, an add + // with a positive delta must be guaranteed to + // occur before Wait() is called on the wg. + // In a way, this kind of acts as a safety barrier. + s.httpWg.Add(1) + // Set up each virtualhost for _, conf := range configs { if _, exists := s.vhosts[conf.Host]; exists { - return nil, fmt.Errorf("cannot serve %s - host already defined for address %s", conf.Address(), s.address) + return nil, fmt.Errorf("cannot serve %s - host already defined for address %s", conf.Address(), s.Addr) } vh := virtualHost{config: conf} @@ -60,98 +96,104 @@ func New(addr string, configs []Config) (*Server, error) { return s, nil } -// Serve starts the server. It blocks until the server quits. -func (s *Server) Serve() error { - server := &http.Server{ - Addr: s.address, - Handler: s, +// Serve starts the server with an existing listener. It blocks until the +// server stops. +func (s *Server) Serve(ln ListenerFile) error { + err := s.setup() + if err != nil { + close(s.startChan) + return err } + return s.serve(ln) +} - if s.HTTP2 { - // TODO: This call may not be necessary after HTTP/2 is merged into std lib - http2.ConfigureServer(server, nil) +// ListenAndServe starts the server with a new listener. It blocks until the server stops. +func (s *Server) ListenAndServe() error { + err := s.setup() + if err != nil { + close(s.startChan) + return err } - for _, vh := range s.vhosts { - // Execute startup functions now - for _, start := range vh.config.Startup { - err := start() - if err != nil { - return err + ln, err := net.Listen("tcp", s.Addr) + if err != nil { + var succeeded bool + if runtime.GOOS == "windows" { // TODO: Limit this to Windows only? (it keeps sockets open after closing listeners) + for i := 0; i < 20; i++ { + time.Sleep(100 * time.Millisecond) + ln, err = net.Listen("tcp", s.Addr) + if err == nil { + succeeded = true + break + } } } - - // Execute shutdown commands on exit - if len(vh.config.Shutdown) > 0 { - go func(vh virtualHost) { - // Wait for signal - interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, os.Interrupt, os.Kill) // TODO: syscall.SIGQUIT? (Ctrl+\, Unix-only) - <-interrupt - - // Run callbacks - exitCode := 0 - for _, shutdownFunc := range vh.config.Shutdown { - err := shutdownFunc() - if err != nil { - exitCode = 1 - log.Println(err) - } - } - os.Exit(exitCode) // BUG: Other shutdown goroutines might be running; use sync.WaitGroup - }(vh) + if !succeeded { + close(s.startChan) + return err } } + return s.serve(ln.(*net.TCPListener)) +} + +// serve prepares s to listen on ln by wrapping ln in a +// tcpKeepAliveListener (if ln is a *net.TCPListener) and +// then in a gracefulListener, so that keep-alive is supported +// as well as graceful shutdown/restart. It also configures +// TLS listener on top of that if applicable. +func (s *Server) serve(ln ListenerFile) error { + if tcpLn, ok := ln.(*net.TCPListener); ok { + ln = tcpKeepAliveListener{TCPListener: tcpLn} + } + + s.listenerMu.Lock() + s.listener = newGracefulListener(ln, &s.httpWg) + s.listenerMu.Unlock() + if s.tls { var tlsConfigs []TLSConfig for _, vh := range s.vhosts { tlsConfigs = append(tlsConfigs, vh.config.TLS) } - return ListenAndServeTLSWithSNI(server, tlsConfigs) + return serveTLSWithSNI(s, s.listener, tlsConfigs) } - return server.ListenAndServe() + + close(s.startChan) // unblock anyone waiting for this to start listening + return s.Server.Serve(s.listener) } -// copy from net/http/transport.go -func cloneTLSConfig(cfg *tls.Config) *tls.Config { - if cfg == nil { - return &tls.Config{} - } - return &tls.Config{ - Rand: cfg.Rand, - Time: cfg.Time, - Certificates: cfg.Certificates, - NameToCertificate: cfg.NameToCertificate, - GetCertificate: cfg.GetCertificate, - RootCAs: cfg.RootCAs, - NextProtos: cfg.NextProtos, - ServerName: cfg.ServerName, - ClientAuth: cfg.ClientAuth, - ClientCAs: cfg.ClientCAs, - InsecureSkipVerify: cfg.InsecureSkipVerify, - CipherSuites: cfg.CipherSuites, - PreferServerCipherSuites: cfg.PreferServerCipherSuites, - SessionTicketsDisabled: cfg.SessionTicketsDisabled, - SessionTicketKey: cfg.SessionTicketKey, - ClientSessionCache: cfg.ClientSessionCache, - MinVersion: cfg.MinVersion, - MaxVersion: cfg.MaxVersion, - CurvePreferences: cfg.CurvePreferences, +// setup prepares the server s to begin listening; it should be +// called just before the listener announces itself on the network +// and should only be called when the server is just starting up. +func (s *Server) setup() error { + if s.HTTP2 { + // TODO: This call may not be necessary after HTTP/2 is merged into std lib + http2.ConfigureServer(s.Server, nil) } -} -// ListenAndServeTLSWithSNI serves TLS with Server Name Indication (SNI) support, which allows -// multiple sites (different hostnames) to be served from the same address. This method is -// adapted directly from the std lib's net/http ListenAndServeTLS function, which was -// written by the Go Authors. It has been modified to support multiple certificate/key pairs. -func ListenAndServeTLSWithSNI(srv *http.Server, tlsConfigs []TLSConfig) error { - addr := srv.Addr - if addr == "" { - addr = ":https" + // Execute startup functions now + for _, vh := range s.vhosts { + for _, startupFunc := range vh.config.Startup { + err := startupFunc() + if err != nil { + return err + } + } } - config := cloneTLSConfig(srv.TLSConfig) + return nil +} + +// serveTLSWithSNI serves TLS with Server Name Indication (SNI) support, which allows +// multiple sites (different hostnames) to be served from the same address. It also +// supports client authentication if srv has it enabled. It blocks until s quits. +// +// This method is adapted from the std lib's net/http ServeTLS function, which was written +// by the Go Authors. It has been modified to support multiple certificate/key pairs, +// client authentication, and our custom Server type. +func serveTLSWithSNI(s *Server, ln net.Listener, tlsConfigs []TLSConfig) error { + config := cloneTLSConfig(s.TLSConfig) if config.NextProtos == nil { config.NextProtos = []string{"http/1.1"} } @@ -162,7 +204,9 @@ func ListenAndServeTLSWithSNI(srv *http.Server, tlsConfigs []TLSConfig) error { config.Certificates = make([]tls.Certificate, len(tlsConfigs)) for i, tlsConfig := range tlsConfigs { config.Certificates[i], err = tls.LoadX509KeyPair(tlsConfig.Certificate, tlsConfig.Key) + config.Certificates[i].OCSPStaple = tlsConfig.OCSPStaple if err != nil { + close(s.startChan) return err } } @@ -177,48 +221,73 @@ func ListenAndServeTLSWithSNI(srv *http.Server, tlsConfigs []TLSConfig) error { // TLS client authentication, if user enabled it err = setupClientAuth(tlsConfigs, config) if err != nil { + close(s.startChan) return err } - // Create listener and we're on our way - conn, err := net.Listen("tcp", addr) - if err != nil { - return err - } - tlsListener := tls.NewListener(conn, config) + // Create TLS listener - note that we do not replace s.listener + // with this TLS listener; tls.listener is unexported and does + // not implement the File() method we need for graceful restarts + // on POSIX systems. + ln = tls.NewListener(ln, config) - return srv.Serve(tlsListener) + close(s.startChan) // unblock anyone waiting for this to start listening + return s.Server.Serve(ln) } -// setupClientAuth sets up TLS client authentication only if -// any of the TLS configs specified at least one cert file. -func setupClientAuth(tlsConfigs []TLSConfig, config *tls.Config) error { - var clientAuth bool - for _, cfg := range tlsConfigs { - if len(cfg.ClientCerts) > 0 { - clientAuth = true - break +// Stop stops the server. It blocks until the server is +// totally stopped. On POSIX systems, it will wait for +// connections to close (up to a max timeout of a few +// seconds); on Windows it will close the listener +// immediately. +func (s *Server) Stop() error { + s.Server.SetKeepAlivesEnabled(false) + + if runtime.GOOS != "windows" { + // force connections to close after timeout + done := make(chan struct{}) + go func() { + s.httpWg.Done() // decrement our initial increment used as a barrier + s.httpWg.Wait() + close(done) + }() + + // Wait for remaining connections to finish or + // force them all to close after timeout + select { + case <-time.After(5 * time.Second): // TODO: make configurable + case <-done: } } - if clientAuth { - pool := x509.NewCertPool() - for _, cfg := range tlsConfigs { - for _, caFile := range cfg.ClientCerts { - caCrt, err := ioutil.ReadFile(caFile) // Anyone that gets a cert from Matt Holt can connect - if err != nil { - return err - } - if !pool.AppendCertsFromPEM(caCrt) { - return fmt.Errorf("error loading client certificate '%s': no certificates were successfully parsed", caFile) - } - } - } - config.ClientCAs = pool - config.ClientAuth = tls.RequireAndVerifyClientCert + // Close the listener now; this stops the server without delay + s.listenerMu.Lock() + err := s.listener.Close() + s.listenerMu.Unlock() + if err != nil { + // TODO: Better logging + log.Println(err) } - return nil + return err +} + +// WaitUntilStarted blocks until the server s is started, meaning +// that practically the next instruction is to start the server loop. +// It also unblocks if the server encounters an error during startup. +func (s *Server) WaitUntilStarted() { + <-s.startChan +} + +// ListenerFd gets the file descriptor of the listener. +func (s *Server) ListenerFd() uintptr { + s.listenerMu.Lock() + defer s.listenerMu.Unlock() + file, err := s.listener.File() + if err != nil { + return 0 + } + return file.Fd() } // ServeHTTP is the entry point for every request to the address that s @@ -260,7 +329,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } else { w.WriteHeader(http.StatusNotFound) - fmt.Fprintf(w, "No such host at %s", s.address) + fmt.Fprintf(w, "No such host at %s", s.Server.Addr) } } @@ -270,3 +339,110 @@ func DefaultErrorFunc(w http.ResponseWriter, r *http.Request, status int) { w.WriteHeader(status) fmt.Fprintf(w, "%d %s", status, http.StatusText(status)) } + +// setupClientAuth sets up TLS client authentication only if +// any of the TLS configs specified at least one cert file. +func setupClientAuth(tlsConfigs []TLSConfig, config *tls.Config) error { + var clientAuth bool + for _, cfg := range tlsConfigs { + if len(cfg.ClientCerts) > 0 { + clientAuth = true + break + } + } + + if clientAuth { + pool := x509.NewCertPool() + for _, cfg := range tlsConfigs { + for _, caFile := range cfg.ClientCerts { + caCrt, err := ioutil.ReadFile(caFile) // Anyone that gets a cert from this CA can connect + if err != nil { + return err + } + if !pool.AppendCertsFromPEM(caCrt) { + return fmt.Errorf("error loading client certificate '%s': no certificates were successfully parsed", caFile) + } + } + } + config.ClientCAs = pool + config.ClientAuth = tls.RequireAndVerifyClientCert + } + + return nil +} + +// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted +// connections. It's used by ListenAndServe and ListenAndServeTLS so +// dead TCP connections (e.g. closing laptop mid-download) eventually +// go away. +// +// Borrowed from the Go standard library. +type tcpKeepAliveListener struct { + *net.TCPListener +} + +// Accept accepts the connection with a keep-alive enabled. +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() + if err != nil { + return + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} + +// File implements ListenerFile; returns the underlying file of the listener. +func (ln tcpKeepAliveListener) File() (*os.File, error) { + return ln.TCPListener.File() +} + +// copied from net/http/transport.go +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} + } + return &tls.Config{ + Rand: cfg.Rand, + Time: cfg.Time, + Certificates: cfg.Certificates, + NameToCertificate: cfg.NameToCertificate, + GetCertificate: cfg.GetCertificate, + RootCAs: cfg.RootCAs, + NextProtos: cfg.NextProtos, + ServerName: cfg.ServerName, + ClientAuth: cfg.ClientAuth, + ClientCAs: cfg.ClientCAs, + InsecureSkipVerify: cfg.InsecureSkipVerify, + CipherSuites: cfg.CipherSuites, + PreferServerCipherSuites: cfg.PreferServerCipherSuites, + SessionTicketsDisabled: cfg.SessionTicketsDisabled, + SessionTicketKey: cfg.SessionTicketKey, + ClientSessionCache: cfg.ClientSessionCache, + MinVersion: cfg.MinVersion, + MaxVersion: cfg.MaxVersion, + CurvePreferences: cfg.CurvePreferences, + } +} + +// ShutdownCallbacks executes all the shutdown callbacks +// for all the virtualhosts in servers, and returns all the +// errors generated during their execution. In other words, +// an error executing one shutdown callback does not stop +// execution of others. Only one shutdown callback is executed +// at a time. You must protect the servers that are passed in +// if they are shared across threads. +func ShutdownCallbacks(servers []*Server) []error { + var errs []error + for _, s := range servers { + for _, vhost := range s.vhosts { + for _, shutdownFunc := range vhost.config.Shutdown { + err := shutdownFunc() + if err != nil { + errs = append(errs, err) + } + } + } + } + return errs +} |