summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--app/app.go76
-rw-r--r--caddy/assets/path.go29
-rw-r--r--caddy/assets/path_test.go12
-rw-r--r--caddy/caddy.go358
-rw-r--r--caddy/caddy_test.go32
-rw-r--r--caddy/caddyfile/json.go163
-rw-r--r--caddy/caddyfile/json_test.go91
-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.go71
-rw-r--r--caddy/letsencrypt/crypto.go30
-rw-r--r--caddy/letsencrypt/crypto_test.go51
-rw-r--r--caddy/letsencrypt/handler.go67
-rw-r--r--caddy/letsencrypt/letsencrypt.go497
-rw-r--r--caddy/letsencrypt/letsencrypt_test.go51
-rw-r--r--caddy/letsencrypt/maintain.go183
-rw-r--r--caddy/letsencrypt/storage.go94
-rw-r--r--caddy/letsencrypt/storage_test.go84
-rw-r--r--caddy/letsencrypt/user.go196
-rw-r--r--caddy/letsencrypt/user_test.go192
-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.go102
-rw-r--r--caddy/restart_windows.go25
-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.go33
-rw-r--r--caddy/sigtrap_posix.go43
-rw-r--r--dist/CHANGES.txt19
-rw-r--r--dist/README.txt2
-rw-r--r--main.go210
-rw-r--r--main_test.go (renamed from app/app_test.go)6
-rw-r--r--middleware/proxy/upstream.go2
-rw-r--r--server/config.go6
-rw-r--r--server/graceful.go76
-rw-r--r--server/server.go402
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{
+ } {
+ 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
diff --git a/main.go b/main.go
index 272cb8fce..3c4930035 100644
--- a/main.go
+++ b/main.go
@@ -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
+}