From b5b31e398ce36c75140987bce1d2094847caa45d Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sun, 25 Oct 2015 18:45:55 -0600 Subject: [PATCH 01/11] letsencrypt: Graceful restarts Lots of refinement still needed and runs only on POSIX systems. Windows will not get true graceful restarts (for now), but we will opt for very, very quick forceful restarts. Also, server configs are no longer put into a map; it is critical that they stay ordered so that they can be matched with their sockets in the child process after forking. This implementation of graceful restarts is probably not perfect, but it is a good start. Lots of details to attend to now. --- app/app.go | 72 +++++++++ config/config.go | 45 ++++-- main.go | 71 ++++++--- server/graceful.go | 70 +++++++++ server/server.go | 372 +++++++++++++++++++++++++++++++-------------- 5 files changed, 486 insertions(+), 144 deletions(-) create mode 100644 server/graceful.go diff --git a/app/app.go b/app/app.go index c63cc833e..3c66e6129 100644 --- a/app/app.go +++ b/app/app.go @@ -7,12 +7,15 @@ package app import ( "errors" + "log" "os" + "os/signal" "path/filepath" "runtime" "strconv" "strings" "sync" + "syscall" "github.com/mholt/caddy/server" ) @@ -42,6 +45,75 @@ var ( Quiet bool ) +func init() { + go func() { + // Wait for signal + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, os.Kill) // TODO: syscall.SIGTERM? Or that should not run callbacks... + <-interrupt + + // Run shutdown callbacks + var exitCode int + ServersMutex.Lock() + errs := server.ShutdownCallbacks(Servers) + ServersMutex.Unlock() + if len(errs) > 0 { + for _, err := range errs { + log.Println(err) + } + exitCode = 1 + } + os.Exit(exitCode) + }() +} + +// 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 caddyfile as its input +// configuration; it will not look elsewhere for the config +// to use. +func Restart(caddyfile []byte) error { + // TODO: This is POSIX-only right now; also, os.Args[0] is required! + // TODO: Pipe the Caddyfile to stdin of child! + // TODO: Before stopping this process, verify child started successfully (valid Caddyfile, etc) + + // Tell the child that it's a restart + os.Setenv("CADDY_RESTART", "true") + + // Pass along current environment and file descriptors to child. + // We pass along the file descriptors explicitly to ensure proper + // order, since losing the original order will break the child. + fds := []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()} + + // Now add file descriptors of the sockets + ServersMutex.Lock() + for _, s := range Servers { + fds = append(fds, s.ListenerFd()) + } + ServersMutex.Unlock() + + // Fork the process with the current environment and file descriptors + execSpec := &syscall.ProcAttr{ + Env: os.Environ(), + Files: fds, + } + fork, err := syscall.ForkExec(os.Args[0], os.Args, execSpec) + if err != nil { + log.Println("FORK ERR:", err, fork) + } + + // Child process is listening now; we can stop all our servers here. + ServersMutex.Lock() + for _, s := range Servers { + go s.Stop() // TODO: error checking/reporting + } + ServersMutex.Unlock() + + return err +} + // 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%). diff --git a/config/config.go b/config/config.go index 97b66ea0f..d7fed8bf2 100644 --- a/config/config.go +++ b/config/config.go @@ -153,14 +153,14 @@ 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) + bindAddr, warnErr, fatalErr := resolveAddr(conf) if fatalErr != nil { - return addresses, fatalErr + return groupings, fatalErr } if warnErr != nil { log.Println("[Warning]", warnErr) @@ -169,37 +169,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 @@ -291,5 +294,15 @@ var ( 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/main.go b/main.go index 2d4c4a031..21a3ae506 100644 --- a/main.go +++ b/main.go @@ -6,12 +6,14 @@ import ( "fmt" "io/ioutil" "log" + "net" "os" "os/exec" "path" "runtime" "strconv" "strings" + "time" "github.com/mholt/caddy/app" "github.com/mholt/caddy/config" @@ -63,44 +65,70 @@ func main() { } // Load config from file - addresses, err := loadConfigs() + groupings, err := loadConfigs() 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) + for i, group := range groupings { + s, err := server.New(group.BindAddr.String(), group.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.Wg.Add(1) + go func(s *server.Server, i int) { + defer app.Wg.Done() + + if os.Getenv("CADDY_RESTART") == "true" { + file := os.NewFile(uintptr(3+i), "") + ln, err := net.FileListener(file) + if err != nil { + log.Fatal("FILE LISTENER:", err) + } + + var ok bool + ln, ok = ln.(server.ListenerFile) + if !ok { + log.Fatal("Listener was not a ListenerFile") + } + + err = s.Serve(ln.(server.ListenerFile)) + // TODO: Better error logging... also, is it even necessary? + if err != nil { + log.Println(err) + } + } else { + err := s.ListenAndServe() + // TODO: Better error logging... also, is it even necessary? + // For example, "use of closed network connection" is normal if doing graceful shutdown... + if err != nil { + log.Println(err) + } + } + }(s, i) + + app.ServersMutex.Lock() app.Servers = append(app.Servers, s) + app.ServersMutex.Unlock() } // Show initialization output if !app.Quiet { var checkedFdLimit bool - for addr, configs := range addresses { - for _, conf := range configs { + 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 addr.IP.IsLoopback() && !isLocalhost(conf.Host) { + if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { fmt.Printf("Notice: %s is only accessible on this machine (%s)\n", - conf.Host, addr.IP.String()) + conf.Host, group.BindAddr.IP.String()) } - if !checkedFdLimit && !addr.IP.IsLoopback() && !isLocalhost(conf.Host) { + if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { checkFdlimit() checkedFdLimit = true } @@ -108,7 +136,16 @@ func main() { } } - // Wait for all listeners to stop + // TODO: Temporary; testing restart + if os.Getenv("CADDY_RESTART") != "true" { + go func() { + time.Sleep(5 * time.Second) + fmt.Println("restarting") + log.Println("RESTART ERR:", app.Restart([]byte{})) + }() + } + + // Wait for all servers to be stopped app.Wg.Wait() } diff --git a/server/graceful.go b/server/graceful.go new file mode 100644 index 000000000..8f74ec96f --- /dev/null +++ b/server/graceful.go @@ -0,0 +1,70 @@ +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 { + c.httpWg.Done() + return c.Conn.Close() +} diff --git a/server/server.go b/server/server.go index 24aa92eb5..9ead46219 100644 --- a/server/server.go +++ b/server/server.go @@ -12,18 +12,31 @@ 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 +} + +type ListenerFile interface { + net.Listener + File() (*os.File, error) } // New creates a new Server which will bind to addr and serve @@ -36,14 +49,30 @@ 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), } + s.Handler = s // TODO: this is weird + // 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. + fmt.Println("+1 (new)") + 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 +89,92 @@ 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 { + return err + } + return s.serve(ln) +} + +// 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 { + return err } - if s.HTTP2 { - // TODO: This call may not be necessary after HTTP/2 is merged into std lib - http2.ConfigureServer(server, nil) + ln, err := net.Listen("tcp", s.Addr) + if err != nil { + return err } - for _, vh := range s.vhosts { - // Execute startup functions now - for _, start := range vh.config.Startup { - err := start() - if err != nil { - return err - } - } + return s.serve(ln.(*net.TCPListener)) +} - // 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) - } +// 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() + + 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{} +// 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) } - 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, + + // Execute startup functions now + for _, vh := range s.vhosts { + for _, startupFunc := range vh.config.Startup { + err := startupFunc() + if err != nil { + return err + } + } } + + return 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 +// 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 { + addr := s.Server.Addr if addr == "" { addr = ":https" } - config := cloneTLSConfig(srv.TLSConfig) + config := cloneTLSConfig(s.TLSConfig) if config.NextProtos == nil { config.NextProtos = []string{"http/1.1"} } @@ -180,45 +203,62 @@ func ListenAndServeTLSWithSNI(srv *http.Server, tlsConfigs []TLSConfig) error { 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) + // Begin serving; block until done + 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) // TODO: Does this even do anything? :P + + 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: 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 and + s.listenerMu.Lock() + err := s.listener.Close() + s.listenerMu.Unlock() + if err != nil { + // TODO: Better logging + log.Println(err) } - return nil + return err +} + +// 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 @@ -226,6 +266,9 @@ func setupClientAuth(tlsConfigs []TLSConfig, config *tls.Config) error { // defined in the Host header so that the correct virtualhost // (configuration and middleware stack) will handle the request. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + fmt.Println("Sleeping") + time.Sleep(5 * time.Second) + fmt.Println("Unblocking") defer func() { // In case the user doesn't enable error middleware, we still // need to make sure that we stay alive up here @@ -260,7 +303,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 +313,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 +} From 69366580192c127dd4f24ad6b3676cd55274292e Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sun, 25 Oct 2015 19:30:29 -0600 Subject: [PATCH 02/11] letsencrypt: Work with latest lego changes --- config/letsencrypt/letsencrypt.go | 6 +++--- config/letsencrypt/renew.go | 4 ++-- main.go | 7 +++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/config/letsencrypt/letsencrypt.go b/config/letsencrypt/letsencrypt.go index 279d23a27..632e80007 100644 --- a/config/letsencrypt/letsencrypt.go +++ b/config/letsencrypt/letsencrypt.go @@ -152,7 +152,7 @@ func newClient(leEmail string) (*acme.Client, error) { } // The client facilitates our communication with the CA server. - client := acme.NewClient(caURL, &leUser, rsaKeySizeToUse, exposePort, true) // TODO: Dev mode is enabled + client := acme.NewClient(caURL, &leUser, rsaKeySizeToUse, exposePort) // If not registered, the user must register an account with the CA // and agree to terms @@ -164,7 +164,7 @@ func newClient(leEmail string) (*acme.Client, error) { leUser.Registration = reg // TODO: we can just do the agreement once: when registering, right? - err = client.AgreeToTos() + 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()) @@ -189,7 +189,7 @@ func obtainCertificates(client *acme.Client, serverConfigs []*server.Config) ([] hosts = append(hosts, cfg.Host) } - certificates, err := client.ObtainCertificates(hosts) + certificates, err := client.ObtainCertificates(hosts, true) if err != nil { return nil, errors.New("error obtaining certs: " + err.Error()) } diff --git a/config/letsencrypt/renew.go b/config/letsencrypt/renew.go index 40f376cbf..291df06c4 100644 --- a/config/letsencrypt/renew.go +++ b/config/letsencrypt/renew.go @@ -84,10 +84,10 @@ func processCertificateRenewal(configs []server.Config) []error { // Renew certificate. // TODO: revokeOld should be an option in the caddyfile - newCertMeta, err := client.RenewCertificate(certMeta, true) + newCertMeta, err := client.RenewCertificate(certMeta, true, true) if err != nil { time.Sleep(10 * time.Second) - newCertMeta, err = client.RenewCertificate(certMeta, true) + newCertMeta, err = client.RenewCertificate(certMeta, true, true) if err != nil { errs = append(errs, err) continue diff --git a/main.go b/main.go index 21a3ae506..b080d663f 100644 --- a/main.go +++ b/main.go @@ -89,13 +89,12 @@ func main() { log.Fatal("FILE LISTENER:", err) } - var ok bool - ln, ok = ln.(server.ListenerFile) + lnf, ok := ln.(server.ListenerFile) if !ok { log.Fatal("Listener was not a ListenerFile") } - err = s.Serve(ln.(server.ListenerFile)) + err = s.Serve(lnf) // TODO: Better error logging... also, is it even necessary? if err != nil { log.Println(err) @@ -204,7 +203,7 @@ func loadConfigs() (config.Group, error) { } } - // 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)) From 4ebff9a13065757988dcec314a9989e9b48a0ed7 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 26 Oct 2015 13:34:31 -0600 Subject: [PATCH 03/11] core: Major refactor for graceful restarts; numerous fixes Merged config and app packages into one called caddy. Abstracted away caddy startup functionality making it easier to embed Caddy in any Go application and use it as a library. Graceful restart (should) now ensure child starts properly. Now piping a gob bundle to child process so that the child can match up inherited listeners to server address. Much cleanup still to do. --- app/app.go | 172 ------- caddy/assets/path.go | 29 ++ caddy/caddy.go | 470 ++++++++++++++++++ {config => caddy}/config.go | 17 +- {config => caddy}/config_test.go | 2 +- {config => caddy}/directives.go | 6 +- {config => caddy}/letsencrypt/crypto.go | 0 {config => caddy}/letsencrypt/crypto_test.go | 0 {config => caddy}/letsencrypt/letsencrypt.go | 6 + .../letsencrypt/letsencrypt_test.go | 0 {config => caddy}/letsencrypt/renew.go | 17 +- {config => caddy}/letsencrypt/storage.go | 4 +- {config => caddy}/letsencrypt/storage_test.go | 0 {config => caddy}/letsencrypt/user.go | 0 {config => caddy}/letsencrypt/user_test.go | 0 {config => caddy}/parse/dispenser.go | 0 {config => caddy}/parse/dispenser_test.go | 0 {config => caddy}/parse/import_test1.txt | 0 {config => caddy}/parse/import_test2.txt | 0 {config => caddy}/parse/lexer.go | 0 {config => caddy}/parse/lexer_test.go | 0 {config => caddy}/parse/parse.go | 0 {config => caddy}/parse/parse_test.go | 0 {config => caddy}/parse/parsing.go | 0 {config => caddy}/parse/parsing_test.go | 0 {config => caddy}/setup/basicauth.go | 0 {config => caddy}/setup/basicauth_test.go | 0 {config => caddy}/setup/bindhost.go | 0 {config => caddy}/setup/browse.go | 0 {config => caddy}/setup/controller.go | 2 +- {config => caddy}/setup/errors.go | 0 {config => caddy}/setup/errors_test.go | 0 {config => caddy}/setup/ext.go | 0 {config => caddy}/setup/ext_test.go | 0 {config => caddy}/setup/fastcgi.go | 0 {config => caddy}/setup/fastcgi_test.go | 0 {config => caddy}/setup/gzip.go | 0 {config => caddy}/setup/gzip_test.go | 0 {config => caddy}/setup/headers.go | 0 {config => caddy}/setup/headers_test.go | 0 {config => caddy}/setup/internal.go | 0 {config => caddy}/setup/internal_test.go | 0 {config => caddy}/setup/log.go | 0 {config => caddy}/setup/log_test.go | 0 {config => caddy}/setup/markdown.go | 0 {config => caddy}/setup/markdown_test.go | 0 {config => caddy}/setup/mime.go | 0 {config => caddy}/setup/mime_test.go | 0 {config => caddy}/setup/proxy.go | 0 {config => caddy}/setup/redir.go | 0 {config => caddy}/setup/rewrite.go | 0 {config => caddy}/setup/rewrite_test.go | 0 {config => caddy}/setup/roller.go | 0 {config => caddy}/setup/root.go | 0 {config => caddy}/setup/root_test.go | 0 {config => caddy}/setup/startupshutdown.go | 0 {config => caddy}/setup/templates.go | 0 {config => caddy}/setup/templates_test.go | 0 .../setup/testdata/blog/first_post.md | 0 {config => caddy}/setup/testdata/header.html | 0 .../setup/testdata/tpl_with_include.html | 0 {config => caddy}/setup/tls.go | 0 {config => caddy}/setup/tls_test.go | 0 {config => caddy}/setup/websocket.go | 0 {config => caddy}/setup/websocket_test.go | 0 main.go | 237 ++++----- middleware/proxy/upstream.go | 2 +- server/graceful.go | 8 +- server/server.go | 10 +- 69 files changed, 630 insertions(+), 352 deletions(-) delete mode 100644 app/app.go create mode 100644 caddy/assets/path.go create mode 100644 caddy/caddy.go rename {config => caddy}/config.go (96%) rename {config => caddy}/config_test.go (99%) rename {config => caddy}/directives.go (96%) rename {config => caddy}/letsencrypt/crypto.go (100%) rename {config => caddy}/letsencrypt/crypto_test.go (100%) rename {config => caddy}/letsencrypt/letsencrypt.go (97%) rename {config => caddy}/letsencrypt/letsencrypt_test.go (100%) rename {config => caddy}/letsencrypt/renew.go (88%) rename {config => caddy}/letsencrypt/storage.go (95%) rename {config => caddy}/letsencrypt/storage_test.go (100%) rename {config => caddy}/letsencrypt/user.go (100%) rename {config => caddy}/letsencrypt/user_test.go (100%) rename {config => caddy}/parse/dispenser.go (100%) rename {config => caddy}/parse/dispenser_test.go (100%) rename {config => caddy}/parse/import_test1.txt (100%) rename {config => caddy}/parse/import_test2.txt (100%) rename {config => caddy}/parse/lexer.go (100%) rename {config => caddy}/parse/lexer_test.go (100%) rename {config => caddy}/parse/parse.go (100%) rename {config => caddy}/parse/parse_test.go (100%) rename {config => caddy}/parse/parsing.go (100%) rename {config => caddy}/parse/parsing_test.go (100%) rename {config => caddy}/setup/basicauth.go (100%) rename {config => caddy}/setup/basicauth_test.go (100%) rename {config => caddy}/setup/bindhost.go (100%) rename {config => caddy}/setup/browse.go (100%) rename {config => caddy}/setup/controller.go (98%) rename {config => caddy}/setup/errors.go (100%) rename {config => caddy}/setup/errors_test.go (100%) rename {config => caddy}/setup/ext.go (100%) rename {config => caddy}/setup/ext_test.go (100%) rename {config => caddy}/setup/fastcgi.go (100%) rename {config => caddy}/setup/fastcgi_test.go (100%) rename {config => caddy}/setup/gzip.go (100%) rename {config => caddy}/setup/gzip_test.go (100%) rename {config => caddy}/setup/headers.go (100%) rename {config => caddy}/setup/headers_test.go (100%) rename {config => caddy}/setup/internal.go (100%) rename {config => caddy}/setup/internal_test.go (100%) rename {config => caddy}/setup/log.go (100%) rename {config => caddy}/setup/log_test.go (100%) rename {config => caddy}/setup/markdown.go (100%) rename {config => caddy}/setup/markdown_test.go (100%) rename {config => caddy}/setup/mime.go (100%) rename {config => caddy}/setup/mime_test.go (100%) rename {config => caddy}/setup/proxy.go (100%) rename {config => caddy}/setup/redir.go (100%) rename {config => caddy}/setup/rewrite.go (100%) rename {config => caddy}/setup/rewrite_test.go (100%) rename {config => caddy}/setup/roller.go (100%) rename {config => caddy}/setup/root.go (100%) rename {config => caddy}/setup/root_test.go (100%) rename {config => caddy}/setup/startupshutdown.go (100%) rename {config => caddy}/setup/templates.go (100%) rename {config => caddy}/setup/templates_test.go (100%) rename {config => caddy}/setup/testdata/blog/first_post.md (100%) rename {config => caddy}/setup/testdata/header.html (100%) rename {config => caddy}/setup/testdata/tpl_with_include.html (100%) rename {config => caddy}/setup/tls.go (100%) rename {config => caddy}/setup/tls_test.go (100%) rename {config => caddy}/setup/websocket.go (100%) rename {config => caddy}/setup/websocket_test.go (100%) diff --git a/app/app.go b/app/app.go deleted file mode 100644 index 3c66e6129..000000000 --- a/app/app.go +++ /dev/null @@ -1,172 +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" - "log" - "os" - "os/signal" - "path/filepath" - "runtime" - "strconv" - "strings" - "sync" - "syscall" - - "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 -) - -func init() { - go func() { - // Wait for signal - interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, os.Interrupt, os.Kill) // TODO: syscall.SIGTERM? Or that should not run callbacks... - <-interrupt - - // Run shutdown callbacks - var exitCode int - ServersMutex.Lock() - errs := server.ShutdownCallbacks(Servers) - ServersMutex.Unlock() - if len(errs) > 0 { - for _, err := range errs { - log.Println(err) - } - exitCode = 1 - } - os.Exit(exitCode) - }() -} - -// 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 caddyfile as its input -// configuration; it will not look elsewhere for the config -// to use. -func Restart(caddyfile []byte) error { - // TODO: This is POSIX-only right now; also, os.Args[0] is required! - // TODO: Pipe the Caddyfile to stdin of child! - // TODO: Before stopping this process, verify child started successfully (valid Caddyfile, etc) - - // Tell the child that it's a restart - os.Setenv("CADDY_RESTART", "true") - - // Pass along current environment and file descriptors to child. - // We pass along the file descriptors explicitly to ensure proper - // order, since losing the original order will break the child. - fds := []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()} - - // Now add file descriptors of the sockets - ServersMutex.Lock() - for _, s := range Servers { - fds = append(fds, s.ListenerFd()) - } - ServersMutex.Unlock() - - // Fork the process with the current environment and file descriptors - execSpec := &syscall.ProcAttr{ - Env: os.Environ(), - Files: fds, - } - fork, err := syscall.ForkExec(os.Args[0], os.Args, execSpec) - if err != nil { - log.Println("FORK ERR:", err, fork) - } - - // Child process is listening now; we can stop all our servers here. - ServersMutex.Lock() - for _, s := range Servers { - go s.Stop() // TODO: error checking/reporting - } - ServersMutex.Unlock() - - return err -} - -// 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 -} - -// DataFolder returns the path to the folder -// where the application may store data. This -// currently resolves to ~/.caddy -func DataFolder() 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.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/caddy.go b/caddy/caddy.go new file mode 100644 index 000000000..1cc039b64 --- /dev/null +++ b/caddy/caddy.go @@ -0,0 +1,470 @@ +package caddy + +import ( + "bytes" + "encoding/gob" + "errors" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "os/signal" + "path" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + + "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 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 = "." +) + +// 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. +type caddyfileGob struct { + ListenerFds map[string]uintptr + Caddyfile []byte +} + +// 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. +// +// If this process is a fork and no Caddyfile was piped in, +// an error will be returned. 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 { + 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() + + groupings, err := Load(path.Base(caddyfile.Path()), bytes.NewReader(caddyfile.Body())) + + // Start each server with its one or more configurations + for i, 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("FILE LISTENER:", 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, i int, ln server.ListenerFile) { + defer wg.Done() + if ln == nil { + err := s.ListenAndServe() + // "use of closed network connection" is normal if doing graceful shutdown... + if !strings.Contains(err.Error(), "use of closed network connection") { + // But an error at initial startup must be fatal + log.Fatal(err) + } + } else { + err := s.Serve(ln) + if err != nil { + log.Println(err) + } + } + }(s, i, ln) + + serversMu.Lock() + servers = append(servers, s) + serversMu.Unlock() + } + + // 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 we're A-OK + if isRestart() { + file := os.NewFile(3, "") + file.Write([]byte("success")) + file.Close() + } + + return nil +} + +// 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.") +} + +// 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) + } + } + } +} + +func Stop() error { + serversMu.Lock() + for _, s := range servers { + s.Stop() // TODO: error checking/reporting? + } + serversMu.Unlock() + return nil +} + +// 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. +func Restart(newCaddyfile Input) error { + if newCaddyfile == nil { + caddyfileMu.Lock() + newCaddyfile = caddyfile + caddyfileMu.Unlock() + } + + if runtime.GOOS == "windows" { + err := Stop() + if err != nil { + return err + } + err = Start(newCaddyfile) + if err != nil { + return err + } + return nil + } + + if len(os.Args) == 0 { // this should never happen, but just in case... + 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.Body(), + } + + // 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, + } + pid, err := syscall.ForkExec(os.Args[0], os.Args, execSpec) + if err != nil { + log.Println("FORK ERR:", err, pid) + } + + // 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 + answer, err := ioutil.ReadAll(sigrpipe) + if err != nil || len(answer) == 0 { + log.Println("restart: child failed to answer; changes not applied") + return incompleteRestartErr + } + + // Child process is listening now; we can stop all our servers here. + return Stop() +} + +// 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. +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 = CaddyfileInput{ + Filepath: os.Stdin.Name(), + Contents: 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 +} + +// Caddyfile returns the current Caddyfile +func Caddyfile() Input { + caddyfileMu.Lock() + defer caddyfileMu.Unlock() + return caddyfile +} + +// 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" +} + +// 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 +} + +// 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 +} + +// CaddyfileInput represents a Caddyfile as input +// and is simply a convenient way to implement +// the Input interface. +type CaddyfileInput struct { + Filepath string + Contents []byte +} + +// Body returns c.Contents. +func (c CaddyfileInput) Body() []byte { return c.Contents } + +// Path returns c.Filepath. +func (c CaddyfileInput) Path() string { return c.Filepath } + +func init() { + letsencrypt.OnRenew = func() error { return Restart(nil) } + + // Trap signals + go func() { + // Wait for signal + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, os.Kill) + <-interrupt + + // TODO: A signal just for graceful restart (reload config) - maybe SIGUSR1 + + // Run shutdown callbacks + 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/config/config.go b/caddy/config.go similarity index 96% rename from config/config.go rename to caddy/config.go index d7fed8bf2..dac657845 100644 --- a/config/config.go +++ b/caddy/config.go @@ -1,4 +1,4 @@ -package config +package caddy import ( "fmt" @@ -7,19 +7,14 @@ import ( "net" "sync" - "github.com/mholt/caddy/app" - "github.com/mholt/caddy/config/letsencrypt" - "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" @@ -56,8 +51,8 @@ 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. diff --git a/config/config_test.go b/caddy/config_test.go similarity index 99% rename from config/config_test.go rename to caddy/config_test.go index 756784191..477da2071 100644 --- a/config/config_test.go +++ b/caddy/config_test.go @@ -1,4 +1,4 @@ -package config +package caddy import ( "testing" diff --git a/config/directives.go b/caddy/directives.go similarity index 96% rename from config/directives.go rename to caddy/directives.go index 354b55959..3ebee7955 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" ) diff --git a/config/letsencrypt/crypto.go b/caddy/letsencrypt/crypto.go similarity index 100% rename from config/letsencrypt/crypto.go rename to caddy/letsencrypt/crypto.go diff --git a/config/letsencrypt/crypto_test.go b/caddy/letsencrypt/crypto_test.go similarity index 100% rename from config/letsencrypt/crypto_test.go rename to caddy/letsencrypt/crypto_test.go diff --git a/config/letsencrypt/letsencrypt.go b/caddy/letsencrypt/letsencrypt.go similarity index 97% rename from config/letsencrypt/letsencrypt.go rename to caddy/letsencrypt/letsencrypt.go index 632e80007..a7aef7e83 100644 --- a/config/letsencrypt/letsencrypt.go +++ b/caddy/letsencrypt/letsencrypt.go @@ -18,6 +18,12 @@ import ( "github.com/xenolf/lego/acme" ) +// OnRenew is the 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, this function will be called. +var OnRenew func() error + // 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 or if plaintext http is explicitly diff --git a/config/letsencrypt/letsencrypt_test.go b/caddy/letsencrypt/letsencrypt_test.go similarity index 100% rename from config/letsencrypt/letsencrypt_test.go rename to caddy/letsencrypt/letsencrypt_test.go diff --git a/config/letsencrypt/renew.go b/caddy/letsencrypt/renew.go similarity index 88% rename from config/letsencrypt/renew.go rename to caddy/letsencrypt/renew.go index 291df06c4..cd19c24e7 100644 --- a/config/letsencrypt/renew.go +++ b/caddy/letsencrypt/renew.go @@ -17,10 +17,16 @@ import ( func keepCertificatesRenewed(configs []server.Config) { ticker := time.Tick(renewInterval) for range ticker { - if errs := processCertificateRenewal(configs); len(errs) > 0 { + if n, errs := processCertificateRenewal(configs); len(errs) > 0 { for _, err := range errs { log.Printf("[ERROR] cert renewal: %v\n", err) } + if n > 0 && OnRenew != nil { + err := OnRenew() + if err != nil { + log.Printf("[ERROR] onrenew callback: %v\n", err) + } + } } } } @@ -28,9 +34,11 @@ func keepCertificatesRenewed(configs []server.Config) { // checkCertificateRenewal loops through all configured // sites and looks for certificates to renew. Nothing is mutated // through this function. The changes happen directly on disk. -func processCertificateRenewal(configs []server.Config) []error { - var errs []error +// It returns the number of certificates renewed and +func processCertificateRenewal(configs []server.Config) (int, []error) { log.Print("[INFO] Processing certificate renewals...") + var errs []error + var n int for _, cfg := range configs { // Host must be TLS-enabled and have assets managed by LE @@ -95,11 +103,12 @@ func processCertificateRenewal(configs []server.Config) []error { } saveCertsAndKeys([]acme.CertificateResource{newCertMeta}) + n++ } else if daysLeft <= 14 { // Warn on 14 days remaining log.Printf("[WARN] There are %d days left on the certificate for %s. Will renew when 7 days remain.\n", daysLeft, cfg.Host) } } - return errs + return n, errs } diff --git a/config/letsencrypt/storage.go b/caddy/letsencrypt/storage.go similarity index 95% rename from config/letsencrypt/storage.go rename to caddy/letsencrypt/storage.go index ca4405a8d..6826e930f 100644 --- a/config/letsencrypt/storage.go +++ b/caddy/letsencrypt/storage.go @@ -4,13 +4,13 @@ import ( "path/filepath" "strings" - "github.com/mholt/caddy/app" + "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(app.DataFolder(), "letsencrypt")) +var storage = Storage(filepath.Join(assets.Path(), "letsencrypt")) // Storage is a root directory and facilitates // forming file paths derived from it. diff --git a/config/letsencrypt/storage_test.go b/caddy/letsencrypt/storage_test.go similarity index 100% rename from config/letsencrypt/storage_test.go rename to caddy/letsencrypt/storage_test.go diff --git a/config/letsencrypt/user.go b/caddy/letsencrypt/user.go similarity index 100% rename from config/letsencrypt/user.go rename to caddy/letsencrypt/user.go diff --git a/config/letsencrypt/user_test.go b/caddy/letsencrypt/user_test.go similarity index 100% rename from config/letsencrypt/user_test.go rename to caddy/letsencrypt/user_test.go diff --git a/config/parse/dispenser.go b/caddy/parse/dispenser.go similarity index 100% rename from config/parse/dispenser.go rename to caddy/parse/dispenser.go diff --git a/config/parse/dispenser_test.go b/caddy/parse/dispenser_test.go similarity index 100% rename from config/parse/dispenser_test.go rename to caddy/parse/dispenser_test.go diff --git a/config/parse/import_test1.txt b/caddy/parse/import_test1.txt similarity index 100% rename from config/parse/import_test1.txt rename to caddy/parse/import_test1.txt diff --git a/config/parse/import_test2.txt b/caddy/parse/import_test2.txt similarity index 100% rename from config/parse/import_test2.txt rename to caddy/parse/import_test2.txt diff --git a/config/parse/lexer.go b/caddy/parse/lexer.go similarity index 100% rename from config/parse/lexer.go rename to caddy/parse/lexer.go diff --git a/config/parse/lexer_test.go b/caddy/parse/lexer_test.go similarity index 100% rename from config/parse/lexer_test.go rename to caddy/parse/lexer_test.go diff --git a/config/parse/parse.go b/caddy/parse/parse.go similarity index 100% rename from config/parse/parse.go rename to caddy/parse/parse.go diff --git a/config/parse/parse_test.go b/caddy/parse/parse_test.go similarity index 100% rename from config/parse/parse_test.go rename to caddy/parse/parse_test.go diff --git a/config/parse/parsing.go b/caddy/parse/parsing.go similarity index 100% rename from config/parse/parsing.go rename to caddy/parse/parsing.go diff --git a/config/parse/parsing_test.go b/caddy/parse/parsing_test.go similarity index 100% rename from config/parse/parsing_test.go rename to caddy/parse/parsing_test.go diff --git a/config/setup/basicauth.go b/caddy/setup/basicauth.go similarity index 100% rename from config/setup/basicauth.go rename to caddy/setup/basicauth.go diff --git a/config/setup/basicauth_test.go b/caddy/setup/basicauth_test.go similarity index 100% rename from config/setup/basicauth_test.go rename to caddy/setup/basicauth_test.go diff --git a/config/setup/bindhost.go b/caddy/setup/bindhost.go similarity index 100% rename from config/setup/bindhost.go rename to caddy/setup/bindhost.go diff --git a/config/setup/browse.go b/caddy/setup/browse.go similarity index 100% rename from config/setup/browse.go rename to caddy/setup/browse.go diff --git a/config/setup/controller.go b/caddy/setup/controller.go similarity index 98% rename from config/setup/controller.go rename to caddy/setup/controller.go index 04873082b..02b366cd8 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 similarity index 100% rename from config/setup/errors.go rename to caddy/setup/errors.go diff --git a/config/setup/errors_test.go b/caddy/setup/errors_test.go similarity index 100% rename from config/setup/errors_test.go rename to caddy/setup/errors_test.go diff --git a/config/setup/ext.go b/caddy/setup/ext.go similarity index 100% rename from config/setup/ext.go rename to caddy/setup/ext.go diff --git a/config/setup/ext_test.go b/caddy/setup/ext_test.go similarity index 100% rename from config/setup/ext_test.go rename to caddy/setup/ext_test.go diff --git a/config/setup/fastcgi.go b/caddy/setup/fastcgi.go similarity index 100% rename from config/setup/fastcgi.go rename to caddy/setup/fastcgi.go diff --git a/config/setup/fastcgi_test.go b/caddy/setup/fastcgi_test.go similarity index 100% rename from config/setup/fastcgi_test.go rename to caddy/setup/fastcgi_test.go diff --git a/config/setup/gzip.go b/caddy/setup/gzip.go similarity index 100% rename from config/setup/gzip.go rename to caddy/setup/gzip.go diff --git a/config/setup/gzip_test.go b/caddy/setup/gzip_test.go similarity index 100% rename from config/setup/gzip_test.go rename to caddy/setup/gzip_test.go diff --git a/config/setup/headers.go b/caddy/setup/headers.go similarity index 100% rename from config/setup/headers.go rename to caddy/setup/headers.go diff --git a/config/setup/headers_test.go b/caddy/setup/headers_test.go similarity index 100% rename from config/setup/headers_test.go rename to caddy/setup/headers_test.go diff --git a/config/setup/internal.go b/caddy/setup/internal.go similarity index 100% rename from config/setup/internal.go rename to caddy/setup/internal.go diff --git a/config/setup/internal_test.go b/caddy/setup/internal_test.go similarity index 100% rename from config/setup/internal_test.go rename to caddy/setup/internal_test.go diff --git a/config/setup/log.go b/caddy/setup/log.go similarity index 100% rename from config/setup/log.go rename to caddy/setup/log.go diff --git a/config/setup/log_test.go b/caddy/setup/log_test.go similarity index 100% rename from config/setup/log_test.go rename to caddy/setup/log_test.go diff --git a/config/setup/markdown.go b/caddy/setup/markdown.go similarity index 100% rename from config/setup/markdown.go rename to caddy/setup/markdown.go diff --git a/config/setup/markdown_test.go b/caddy/setup/markdown_test.go similarity index 100% rename from config/setup/markdown_test.go rename to caddy/setup/markdown_test.go diff --git a/config/setup/mime.go b/caddy/setup/mime.go similarity index 100% rename from config/setup/mime.go rename to caddy/setup/mime.go diff --git a/config/setup/mime_test.go b/caddy/setup/mime_test.go similarity index 100% rename from config/setup/mime_test.go rename to caddy/setup/mime_test.go diff --git a/config/setup/proxy.go b/caddy/setup/proxy.go similarity index 100% rename from config/setup/proxy.go rename to caddy/setup/proxy.go diff --git a/config/setup/redir.go b/caddy/setup/redir.go similarity index 100% rename from config/setup/redir.go rename to caddy/setup/redir.go diff --git a/config/setup/rewrite.go b/caddy/setup/rewrite.go similarity index 100% rename from config/setup/rewrite.go rename to caddy/setup/rewrite.go diff --git a/config/setup/rewrite_test.go b/caddy/setup/rewrite_test.go similarity index 100% rename from config/setup/rewrite_test.go rename to caddy/setup/rewrite_test.go diff --git a/config/setup/roller.go b/caddy/setup/roller.go similarity index 100% rename from config/setup/roller.go rename to caddy/setup/roller.go diff --git a/config/setup/root.go b/caddy/setup/root.go similarity index 100% rename from config/setup/root.go rename to caddy/setup/root.go diff --git a/config/setup/root_test.go b/caddy/setup/root_test.go similarity index 100% rename from config/setup/root_test.go rename to caddy/setup/root_test.go diff --git a/config/setup/startupshutdown.go b/caddy/setup/startupshutdown.go similarity index 100% rename from config/setup/startupshutdown.go rename to caddy/setup/startupshutdown.go diff --git a/config/setup/templates.go b/caddy/setup/templates.go similarity index 100% rename from config/setup/templates.go rename to caddy/setup/templates.go diff --git a/config/setup/templates_test.go b/caddy/setup/templates_test.go similarity index 100% rename from config/setup/templates_test.go rename to caddy/setup/templates_test.go diff --git a/config/setup/testdata/blog/first_post.md b/caddy/setup/testdata/blog/first_post.md similarity index 100% rename from config/setup/testdata/blog/first_post.md rename to caddy/setup/testdata/blog/first_post.md diff --git a/config/setup/testdata/header.html b/caddy/setup/testdata/header.html similarity index 100% rename from config/setup/testdata/header.html rename to caddy/setup/testdata/header.html diff --git a/config/setup/testdata/tpl_with_include.html b/caddy/setup/testdata/tpl_with_include.html similarity index 100% rename from config/setup/testdata/tpl_with_include.html rename to caddy/setup/testdata/tpl_with_include.html diff --git a/config/setup/tls.go b/caddy/setup/tls.go similarity index 100% rename from config/setup/tls.go rename to caddy/setup/tls.go diff --git a/config/setup/tls_test.go b/caddy/setup/tls_test.go similarity index 100% rename from config/setup/tls_test.go rename to caddy/setup/tls_test.go diff --git a/config/setup/websocket.go b/caddy/setup/websocket.go similarity index 100% rename from config/setup/websocket.go rename to caddy/setup/websocket.go diff --git a/config/setup/websocket_test.go b/caddy/setup/websocket_test.go similarity index 100% rename from config/setup/websocket_test.go rename to caddy/setup/websocket_test.go diff --git a/main.go b/main.go index b080d663f..68aab11c4 100644 --- a/main.go +++ b/main.go @@ -1,24 +1,19 @@ package main import ( - "bytes" + "errors" "flag" "fmt" "io/ioutil" "log" - "net" "os" - "os/exec" - "path" "runtime" "strconv" "strings" "time" - "github.com/mholt/caddy/app" - "github.com/mholt/caddy/config" - "github.com/mholt/caddy/config/letsencrypt" - "github.com/mholt/caddy/server" + "github.com/mholt/caddy/caddy" + "github.com/mholt/caddy/caddy/letsencrypt" ) var ( @@ -28,25 +23,33 @@ var ( 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, "Enable 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") flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement") flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default email address to use for Let's Encrypt transactions") - flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") + flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke its 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 != "" { @@ -59,165 +62,103 @@ func main() { } // Set CPU cap - err := app.SetCPU(cpu) + err := setCPU(cpu) if err != nil { log.Fatal(err) } - // Load config from file - groupings, 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 i, group := range groupings { - s, err := server.New(group.BindAddr.String(), group.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, i int) { - defer app.Wg.Done() - - if os.Getenv("CADDY_RESTART") == "true" { - file := os.NewFile(uintptr(3+i), "") - ln, err := net.FileListener(file) - if err != nil { - log.Fatal("FILE LISTENER:", err) - } - - lnf, ok := ln.(server.ListenerFile) - if !ok { - log.Fatal("Listener was not a ListenerFile") - } - - err = s.Serve(lnf) - // TODO: Better error logging... also, is it even necessary? - if err != nil { - log.Println(err) - } - } else { - err := s.ListenAndServe() - // TODO: Better error logging... also, is it even necessary? - // For example, "use of closed network connection" is normal if doing graceful shutdown... - if err != nil { - log.Println(err) - } - } - }(s, i) - - app.ServersMutex.Lock() - app.Servers = append(app.Servers, s) - app.ServersMutex.Unlock() - } - - // Show initialization output - if !app.Quiet { - 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 - } - } - } + // Start your engines + err = caddy.Start(caddyfile) + if err != nil { + log.Fatal(err) } // TODO: Temporary; testing restart - if os.Getenv("CADDY_RESTART") != "true" { - go func() { - time.Sleep(5 * time.Second) - fmt.Println("restarting") - log.Println("RESTART ERR:", app.Restart([]byte{})) - }() - } + //if os.Getenv("CADDY_RESTART") != "true" { + go func() { + time.Sleep(5 * time.Second) + fmt.Println("restarting") + log.Println("RESTART ERR:", caddy.Restart(nil)) + }() + //} - // Wait for all servers to be stopped - app.Wg.Wait() + // Twiddle your thumbs + caddy.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) - } - } - } -} - -// 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.") -} - -// 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, + }, 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 args + // 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 config.Load(config.DefaultConfigFile, file) + return caddy.CaddyfileInput{ + Contents: contents, + Filepath: caddy.DefaultConfigFile, + }, 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 + } + + runtime.GOMAXPROCS(numCPU) + return nil } 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/graceful.go b/server/graceful.go index 8f74ec96f..6b2ae4f5c 100644 --- a/server/graceful.go +++ b/server/graceful.go @@ -65,6 +65,12 @@ type gracefulConn struct { // 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 c.Conn.Close() + return nil } diff --git a/server/server.go b/server/server.go index 9ead46219..befbe86c4 100644 --- a/server/server.go +++ b/server/server.go @@ -59,14 +59,13 @@ func New(addr string, configs []Config) (*Server, error) { tls: tls, vhosts: make(map[string]virtualHost), } - s.Handler = s // TODO: this is weird + 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. - fmt.Println("+1 (new)") s.httpWg.Add(1) // Set up each virtualhost @@ -169,11 +168,6 @@ func (s *Server) setup() error { // 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 { - addr := s.Server.Addr - if addr == "" { - addr = ":https" - } - config := cloneTLSConfig(s.TLSConfig) if config.NextProtos == nil { config.NextProtos = []string{"http/1.1"} @@ -267,7 +261,7 @@ func (s *Server) ListenerFd() uintptr { // (configuration and middleware stack) will handle the request. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Println("Sleeping") - time.Sleep(5 * time.Second) + time.Sleep(5 * time.Second) // TODO: Temporarily making requests hang so we can test graceful restart fmt.Println("Unblocking") defer func() { // In case the user doesn't enable error middleware, we still From 41c4484222ecd9efc4b5f48c3c66a5e87a2ff532 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 26 Oct 2015 14:28:29 -0600 Subject: [PATCH 04/11] core: SIGUSR1 to reload config; some code cleanup --- caddy/caddy.go | 159 +++++++++++++++++++++++++++-------------------- main.go | 10 --- server/server.go | 3 - 3 files changed, 91 insertions(+), 81 deletions(-) diff --git a/caddy/caddy.go b/caddy/caddy.go index 1cc039b64..ac0996903 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -105,58 +105,15 @@ func Start(cdyfile Input) error { caddyfile = cdyfile caddyfileMu.Unlock() - groupings, err := Load(path.Base(caddyfile.Path()), bytes.NewReader(caddyfile.Body())) + groupings, err := Load(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body())) + if err != nil { + return err + } // Start each server with its one or more configurations - for i, 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("FILE LISTENER:", 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, i int, ln server.ListenerFile) { - defer wg.Done() - if ln == nil { - err := s.ListenAndServe() - // "use of closed network connection" is normal if doing graceful shutdown... - if !strings.Contains(err.Error(), "use of closed network connection") { - // But an error at initial startup must be fatal - log.Fatal(err) - } - } else { - err := s.Serve(ln) - if err != nil { - log.Println(err) - } - } - }(s, i, ln) - - serversMu.Lock() - servers = append(servers, s) - serversMu.Unlock() + err = startServers(groupings) + if err != nil { + return err } // Close remaining file descriptors we may have inherited that we don't need @@ -191,7 +148,7 @@ func Start(cdyfile Input) error { } } - // Tell parent we're A-OK + // Tell parent process that we got this if isRestart() { file := os.NewFile(3, "") file.Write([]byte("success")) @@ -201,6 +158,64 @@ func Start(cdyfile Input) error { 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. +func startServers(groupings Group) error { + for i, 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, i int, 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, i, ln) + + serversMu.Lock() + servers = append(servers, s) + serversMu.Unlock() + } + return nil +} + // 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.") @@ -302,9 +317,9 @@ func Restart(newCaddyfile Input) error { Env: os.Environ(), Files: fds, } - pid, err := syscall.ForkExec(os.Args[0], os.Args, execSpec) + _, err = syscall.ForkExec(os.Args[0], os.Args, execSpec) if err != nil { - log.Println("FORK ERR:", err, pid) + return err } // Feed it the Caddyfile @@ -447,24 +462,32 @@ func init() { // Trap signals go func() { - // Wait for signal - interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, os.Interrupt, os.Kill) - <-interrupt + shutdown, reload := make(chan os.Signal, 1), make(chan os.Signal, 1) + signal.Notify(shutdown, os.Interrupt, os.Kill) // quit the process + signal.Notify(reload, syscall.SIGUSR1) // reload configuration - // TODO: A signal just for graceful restart (reload config) - maybe SIGUSR1 + for { + select { + case <-shutdown: + var exitCode int - // Run shutdown callbacks - var exitCode int - serversMu.Lock() - errs := server.ShutdownCallbacks(servers) - serversMu.Unlock() - if len(errs) > 0 { - for _, err := range errs { - log.Println(err) + serversMu.Lock() + errs := server.ShutdownCallbacks(servers) + serversMu.Unlock() + if len(errs) > 0 { + for _, err := range errs { + log.Println(err) + } + exitCode = 1 + } + os.Exit(exitCode) + + case <-reload: + err := Restart(nil) + if err != nil { + log.Println(err) + } } - exitCode = 1 } - os.Exit(exitCode) }() } diff --git a/main.go b/main.go index 68aab11c4..8e4bffc2c 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,6 @@ import ( "runtime" "strconv" "strings" - "time" "github.com/mholt/caddy/caddy" "github.com/mholt/caddy/caddy/letsencrypt" @@ -79,15 +78,6 @@ func main() { log.Fatal(err) } - // TODO: Temporary; testing restart - //if os.Getenv("CADDY_RESTART") != "true" { - go func() { - time.Sleep(5 * time.Second) - fmt.Println("restarting") - log.Println("RESTART ERR:", caddy.Restart(nil)) - }() - //} - // Twiddle your thumbs caddy.Wait() } diff --git a/server/server.go b/server/server.go index befbe86c4..0a4dd4bab 100644 --- a/server/server.go +++ b/server/server.go @@ -260,9 +260,6 @@ func (s *Server) ListenerFd() uintptr { // defined in the Host header so that the correct virtualhost // (configuration and middleware stack) will handle the request. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - fmt.Println("Sleeping") - time.Sleep(5 * time.Second) // TODO: Temporarily making requests hang so we can test graceful restart - fmt.Println("Unblocking") defer func() { // In case the user doesn't enable error middleware, we still // need to make sure that we stay alive up here From 5b1962303dadd158e74757118f7ddcc6f51f1e39 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 26 Oct 2015 14:55:03 -0600 Subject: [PATCH 05/11] core: More refactoring, code cleanup, docs --- caddy/caddy.go | 213 +++++------------------------------------------ caddy/helpers.go | 74 ++++++++++++++++ caddy/restart.go | 132 +++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+), 192 deletions(-) create mode 100644 caddy/helpers.go create mode 100644 caddy/restart.go diff --git a/caddy/caddy.go b/caddy/caddy.go index ac0996903..b80450ee0 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -1,3 +1,16 @@ +// 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. package caddy import ( @@ -9,16 +22,10 @@ import ( "log" "net" "os" - "os/exec" - "os/signal" "path" - "runtime" - "strconv" "strings" "sync" - "syscall" - "github.com/mholt/caddy/caddy/letsencrypt" "github.com/mholt/caddy/server" ) @@ -72,14 +79,6 @@ const ( DefaultRoot = "." ) -// 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. -type caddyfileGob struct { - ListenerFds map[string]uintptr - Caddyfile []byte -} - // 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 @@ -216,28 +215,7 @@ func startServers(groupings Group) error { return nil } -// 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.") -} - -// 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) - } - } - } -} - +// Stop stops all servers. It blocks until they are all stopped. func Stop() error { serversMu.Lock() for _, s := range servers { @@ -247,100 +225,6 @@ func Stop() error { return nil } -// 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. -func Restart(newCaddyfile Input) error { - if newCaddyfile == nil { - caddyfileMu.Lock() - newCaddyfile = caddyfile - caddyfileMu.Unlock() - } - - if runtime.GOOS == "windows" { - err := Stop() - if err != nil { - return err - } - err = Start(newCaddyfile) - if err != nil { - return err - } - return nil - } - - if len(os.Args) == 0 { // this should never happen, but just in case... - 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.Body(), - } - - // 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 - answer, err := ioutil.ReadAll(sigrpipe) - if err != nil || len(answer) == 0 { - log.Println("restart: child failed to answer; changes not applied") - return incompleteRestartErr - } - - // Child process is listening now; we can stop all our servers here. - return Stop() -} - // Wait blocks until all servers are stopped. func Wait() { wg.Wait() @@ -387,19 +271,6 @@ func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) { return } -// Caddyfile returns the current Caddyfile -func Caddyfile() Input { - caddyfileMu.Lock() - defer caddyfileMu.Unlock() - return caddyfile -} - -// 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" -} - // 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 @@ -430,6 +301,13 @@ func CaddyfileFromPipe(f *os.File) (Input, error) { 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 @@ -442,52 +320,3 @@ type Input interface { // Gets the path to the origin file Path() string } - -// CaddyfileInput represents a Caddyfile as input -// and is simply a convenient way to implement -// the Input interface. -type CaddyfileInput struct { - Filepath string - Contents []byte -} - -// Body returns c.Contents. -func (c CaddyfileInput) Body() []byte { return c.Contents } - -// Path returns c.Filepath. -func (c CaddyfileInput) Path() string { return c.Filepath } - -func init() { - letsencrypt.OnRenew = func() error { return Restart(nil) } - - // Trap signals - go func() { - shutdown, reload := make(chan os.Signal, 1), make(chan os.Signal, 1) - signal.Notify(shutdown, os.Interrupt, os.Kill) // quit the process - signal.Notify(reload, syscall.SIGUSR1) // reload configuration - - for { - select { - case <-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) - - case <-reload: - err := Restart(nil) - if err != nil { - log.Println(err) - } - } - } - }() -} diff --git a/caddy/helpers.go b/caddy/helpers.go new file mode 100644 index 000000000..209eb7f82 --- /dev/null +++ b/caddy/helpers.go @@ -0,0 +1,74 @@ +package caddy + +import ( + "bytes" + "fmt" + "log" + "os" + "os/exec" + "os/signal" + "runtime" + "strconv" + "strings" + "syscall" + + "github.com/mholt/caddy/caddy/letsencrypt" + "github.com/mholt/caddy/server" +) + +func init() { + letsencrypt.OnRenew = func() error { return Restart(nil) } + + // Trap signals + go func() { + shutdown, reload := make(chan os.Signal, 1), make(chan os.Signal, 1) + signal.Notify(shutdown, os.Interrupt, os.Kill) // quit the process + signal.Notify(reload, syscall.SIGUSR1) // reload configuration + + for { + select { + case <-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) + + case <-reload: + err := Restart(nil) + if err != nil { + log.Println(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.") +} + +// 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) + } + } + } +} diff --git a/caddy/restart.go b/caddy/restart.go new file mode 100644 index 000000000..43848bf62 --- /dev/null +++ b/caddy/restart.go @@ -0,0 +1,132 @@ +package caddy + +import ( + "encoding/gob" + "io/ioutil" + "log" + "os" + "runtime" + "syscall" +) + +// 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 []byte +} + +// 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. +func Restart(newCaddyfile Input) error { + if newCaddyfile == nil { + caddyfileMu.Lock() + newCaddyfile = caddyfile + caddyfileMu.Unlock() + } + + if runtime.GOOS == "windows" { + err := Stop() + if err != nil { + return err + } + err = Start(newCaddyfile) + if err != nil { + return err + } + return nil + } + + if len(os.Args) == 0 { // this should never happen, but just in case... + 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.Body(), + } + + // 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 + answer, err := ioutil.ReadAll(sigrpipe) + if err != nil || len(answer) == 0 { + log.Println("restart: child failed to answer; changes not applied") + return incompleteRestartErr + } + + // Child process is listening now; we can stop all our servers here. + return Stop() +} + +// 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 +} + +// Body returns c.Contents. +func (c CaddyfileInput) Body() []byte { return c.Contents } + +// Path returns c.Filepath. +func (c CaddyfileInput) Path() string { return c.Filepath } From 821c0fab0996cbc7d8beaecd9abab4736215259a Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 26 Oct 2015 16:49:05 -0600 Subject: [PATCH 06/11] core: Refactoring POSIX-only code for build tags --- caddy/helpers.go | 63 ++++++++++++++++++---------------------- caddy/restart.go | 43 ++------------------------- caddy/restart_windows.go | 25 ++++++++++++++++ caddy/sigtrap.go | 33 +++++++++++++++++++++ caddy/sigtrap_posix.go | 26 +++++++++++++++++ 5 files changed, 114 insertions(+), 76 deletions(-) create mode 100644 caddy/restart_windows.go create mode 100644 caddy/sigtrap.go create mode 100644 caddy/sigtrap_posix.go diff --git a/caddy/helpers.go b/caddy/helpers.go index 209eb7f82..c30d7c168 100644 --- a/caddy/helpers.go +++ b/caddy/helpers.go @@ -3,52 +3,17 @@ package caddy import ( "bytes" "fmt" - "log" "os" "os/exec" - "os/signal" "runtime" "strconv" "strings" - "syscall" "github.com/mholt/caddy/caddy/letsencrypt" - "github.com/mholt/caddy/server" ) func init() { letsencrypt.OnRenew = func() error { return Restart(nil) } - - // Trap signals - go func() { - shutdown, reload := make(chan os.Signal, 1), make(chan os.Signal, 1) - signal.Notify(shutdown, os.Interrupt, os.Kill) // quit the process - signal.Notify(reload, syscall.SIGUSR1) // reload configuration - - for { - select { - case <-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) - - case <-reload: - err := Restart(nil) - if err != nil { - log.Println(err) - } - } - } - }() } // isLocalhost returns true if the string looks explicitly like a localhost address. @@ -72,3 +37,31 @@ func checkFdlimit() { } } } + +// 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 []byte +} + +// 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 +} + +// Body returns c.Contents. +func (c CaddyfileInput) Body() []byte { return c.Contents } + +// Path returns c.Filepath. +func (c CaddyfileInput) Path() string { return c.Filepath } diff --git a/caddy/restart.go b/caddy/restart.go index 43848bf62..7a07fbc1b 100644 --- a/caddy/restart.go +++ b/caddy/restart.go @@ -1,3 +1,5 @@ +// +build !windows + package caddy import ( @@ -5,18 +7,9 @@ import ( "io/ioutil" "log" "os" - "runtime" "syscall" ) -// 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 []byte -} - // 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. @@ -31,18 +24,6 @@ func Restart(newCaddyfile Input) error { caddyfileMu.Unlock() } - if runtime.GOOS == "windows" { - err := Stop() - if err != nil { - return err - } - err = Start(newCaddyfile) - if err != nil { - return err - } - return nil - } - if len(os.Args) == 0 { // this should never happen, but just in case... os.Args = []string{""} } @@ -110,23 +91,3 @@ func Restart(newCaddyfile Input) error { // Child process is listening now; we can stop all our servers here. return Stop() } - -// 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 -} - -// Body returns c.Contents. -func (c CaddyfileInput) Body() []byte { return c.Contents } - -// Path returns c.Filepath. -func (c CaddyfileInput) Path() string { return c.Filepath } 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/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..789985efb --- /dev/null +++ b/caddy/sigtrap_posix.go @@ -0,0 +1,26 @@ +// +build !windows + +package caddy + +import ( + "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 + err := Restart(nil) + if err != nil { + log.Println(err) + } + } + }() +} From bb6613d0ae384574cbc638f3845bbb3250588593 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 26 Oct 2015 17:57:32 -0600 Subject: [PATCH 07/11] core: Fix SIGUSR1 so it actually reloads config --- caddy/caddy.go | 4 ++++ caddy/helpers.go | 4 ++++ caddy/sigtrap_posix.go | 19 ++++++++++++++++++- main.go | 2 ++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/caddy/caddy.go b/caddy/caddy.go index b80450ee0..9383e3860 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -319,4 +319,8 @@ type Input interface { // 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/helpers.go b/caddy/helpers.go index c30d7c168..d8f409708 100644 --- a/caddy/helpers.go +++ b/caddy/helpers.go @@ -58,6 +58,7 @@ func isRestart() bool { type CaddyfileInput struct { Filepath string Contents []byte + RealFile bool } // Body returns c.Contents. @@ -65,3 +66,6 @@ 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/sigtrap_posix.go b/caddy/sigtrap_posix.go index 789985efb..122adf2c3 100644 --- a/caddy/sigtrap_posix.go +++ b/caddy/sigtrap_posix.go @@ -3,6 +3,7 @@ package caddy import ( + "io/ioutil" "log" "os" "os/signal" @@ -17,7 +18,23 @@ func init() { for { <-reload - err := Restart(nil) + + 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(err) } diff --git a/main.go b/main.go index 8e4bffc2c..15d39de55 100644 --- a/main.go +++ b/main.go @@ -92,6 +92,7 @@ func loadCaddyfile() (caddy.Input, error) { return caddy.CaddyfileInput{ Contents: contents, Filepath: conf, + RealFile: true, }, nil } @@ -115,6 +116,7 @@ func loadCaddyfile() (caddy.Input, error) { return caddy.CaddyfileInput{ Contents: contents, Filepath: caddy.DefaultConfigFile, + RealFile: true, }, nil } From c487b702a22c1a28bc0380b3e45a314d586ce0ef Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 27 Oct 2015 00:05:22 -0600 Subject: [PATCH 08/11] Little cleanup --- caddy/caddy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/caddy/caddy.go b/caddy/caddy.go index 9383e3860..f6976ea0a 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -149,9 +149,9 @@ func Start(cdyfile Input) error { // Tell parent process that we got this if isRestart() { - file := os.NewFile(3, "") - file.Write([]byte("success")) - file.Close() + ppipe := os.NewFile(3, "") // parent is listening on pipe at index 3 + ppipe.Write([]byte("success")) + ppipe.Close() } return nil From a6ea1e6b55b840dc3c55dac4dc2f97ae0bfcc64a Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 27 Oct 2015 12:52:58 -0600 Subject: [PATCH 09/11] letsencrypt: -ca flag to customize CA server --- caddy/letsencrypt/letsencrypt.go | 10 ++++------ main.go | 8 ++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/caddy/letsencrypt/letsencrypt.go b/caddy/letsencrypt/letsencrypt.go index a7aef7e83..093a4eb6a 100644 --- a/caddy/letsencrypt/letsencrypt.go +++ b/caddy/letsencrypt/letsencrypt.go @@ -158,7 +158,7 @@ func newClient(leEmail string) (*acme.Client, error) { } // The client facilitates our communication with the CA server. - client := acme.NewClient(caURL, &leUser, rsaKeySizeToUse, exposePort) + client := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse, exposePort) // If not registered, the user must register an account with the CA // and agree to terms @@ -331,15 +331,13 @@ var ( // 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 base URL to the Let's Encrypt CA - // TODO: Staging API URL is: https://acme-staging.api.letsencrypt.org - // TODO: Production endpoint is: https://acme-v01.api.letsencrypt.org - caURL = "http://192.168.99.100:4000" - // The port to expose to the CA server for Simple HTTP Challenge exposePort = "5001" diff --git a/main.go b/main.go index 15d39de55..aa3ed0d83 100644 --- a/main.go +++ b/main.go @@ -29,16 +29,20 @@ const ( func init() { flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")") - flag.BoolVar(&caddy.HTTP2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib + 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(&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 email address to use for Let's Encrypt transactions") - flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke its certificate") + flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") } func main() { From 362ead276098d8f4cdf433a2dba6753962081680 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 27 Oct 2015 12:53:31 -0600 Subject: [PATCH 10/11] Minor test improvements --- caddy/assets/path_test.go | 12 ++++++++++++ caddy/letsencrypt/crypto_test.go | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 caddy/assets/path_test.go 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/letsencrypt/crypto_test.go b/caddy/letsencrypt/crypto_test.go index 938778a8d..7f791a6c3 100644 --- a/caddy/letsencrypt/crypto_test.go +++ b/caddy/letsencrypt/crypto_test.go @@ -10,14 +10,14 @@ import ( ) func init() { - rsaKeySizeToUse = 128 // makes tests faster + 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, 128) // small key size is OK for testing + privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse) if err != nil { t.Fatal(err) } From 8548641dc1225d2423214fa628f1c2b9af0ff457 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 27 Oct 2015 13:02:47 -0600 Subject: [PATCH 11/11] letsencrypt: Check for errors --- caddy/letsencrypt/letsencrypt.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/caddy/letsencrypt/letsencrypt.go b/caddy/letsencrypt/letsencrypt.go index 3edd2b927..ca9a1b897 100644 --- a/caddy/letsencrypt/letsencrypt.go +++ b/caddy/letsencrypt/letsencrypt.go @@ -39,7 +39,7 @@ var OnRenew func() error func Activate(configs []server.Config) ([]server.Config, error) { // First identify and configure any elligible hosts for which // we already have certs and keys in storage from last time. - configLen := len(configs) // avoid infinite loop since this loop appends to the slice + 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) && configs[i].TLS.LetsEncryptEmail != "off" { configs = autoConfigure(&configs[i], configs) @@ -238,9 +238,14 @@ func saveCertsAndKeys(certificates []acme.CertificateResource) error { // autoConfigure enables TLS on cfg and appends, if necessary, a new config // to allConfigs that redirects plaintext HTTP to its new HTTPS counterpart. func autoConfigure(cfg *server.Config, allConfigs []server.Config) []server.Config { - bundleBytes, _ := ioutil.ReadFile(storage.SiteCertFile(cfg.Host)) - ocsp, _ := acme.GetOCSPForCert(bundleBytes) - cfg.TLS.OCSPStaple = ocsp + bundleBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host)) + // TODO: Handle these errors better + if err == nil { + ocsp, err := acme.GetOCSPForCert(bundleBytes) + if err == nil { + cfg.TLS.OCSPStaple = ocsp + } + } cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host) cfg.TLS.Key = storage.SiteKeyFile(cfg.Host) cfg.TLS.Enabled = true