Commit 6762df41 authored by Matthew Holt's avatar Matthew Holt

Clean up leaking goroutines and safer Start()/Stop()

parent 1818b1ea
...@@ -26,6 +26,7 @@ import ( ...@@ -26,6 +26,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/mholt/caddy/caddy/letsencrypt"
"github.com/mholt/caddy/server" "github.com/mholt/caddy/server"
) )
...@@ -90,6 +91,8 @@ const ( ...@@ -90,6 +91,8 @@ const (
// In any case, an error is returned if Caddy could not be // In any case, an error is returned if Caddy could not be
// started. // started.
func Start(cdyfile Input) error { func Start(cdyfile Input) error {
// TODO: What if already started -- is that an error?
var err error var err error
// Input must never be nil; try to load something // Input must never be nil; try to load something
...@@ -104,7 +107,20 @@ func Start(cdyfile Input) error { ...@@ -104,7 +107,20 @@ func Start(cdyfile Input) error {
caddyfile = cdyfile caddyfile = cdyfile
caddyfileMu.Unlock() caddyfileMu.Unlock()
groupings, err := Load(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body())) // load the server configs
configs, err := load(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body()))
if err != nil {
return err
}
// secure all the things
configs, err = letsencrypt.Activate(configs)
if err != nil {
return err
}
// group virtualhosts by address
groupings, err := arrangeBindings(configs)
if err != nil { if err != nil {
return err return err
} }
...@@ -217,11 +233,15 @@ func startServers(groupings Group) error { ...@@ -217,11 +233,15 @@ func startServers(groupings Group) error {
// Stop stops all servers. It blocks until they are all stopped. // Stop stops all servers. It blocks until they are all stopped.
func Stop() error { func Stop() error {
letsencrypt.Deactivate()
serversMu.Lock() serversMu.Lock()
for _, s := range servers { for _, s := range servers {
s.Stop() // TODO: error checking/reporting? s.Stop() // TODO: error checking/reporting?
} }
servers = []*server.Server{} // don't reuse servers
serversMu.Unlock() serversMu.Unlock()
return nil return nil
} }
......
...@@ -7,7 +7,6 @@ import ( ...@@ -7,7 +7,6 @@ import (
"net" "net"
"sync" "sync"
"github.com/mholt/caddy/caddy/letsencrypt"
"github.com/mholt/caddy/caddy/parse" "github.com/mholt/caddy/caddy/parse"
"github.com/mholt/caddy/caddy/setup" "github.com/mholt/caddy/caddy/setup"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
...@@ -20,9 +19,9 @@ const ( ...@@ -20,9 +19,9 @@ const (
DefaultConfigFile = "Caddyfile" DefaultConfigFile = "Caddyfile"
) )
// Load reads input (named filename) and parses it, returning server // load reads input (named filename) and parses it, returning the
// configurations grouped by listening address. // server configurations in the order they appeared in the input.
func Load(filename string, input io.Reader) (Group, error) { func load(filename string, input io.Reader) ([]server.Config, error) {
var configs []server.Config var configs []server.Config
// turn off timestamp for parsing // turn off timestamp for parsing
...@@ -34,7 +33,7 @@ func Load(filename string, input io.Reader) (Group, error) { ...@@ -34,7 +33,7 @@ func Load(filename string, input io.Reader) (Group, error) {
return nil, err return nil, err
} }
if len(serverBlocks) == 0 { if len(serverBlocks) == 0 {
return Default() return []server.Config{NewDefault()}, nil
} }
// Each server block represents similar hosts/addresses. // Each server block represents similar hosts/addresses.
...@@ -101,14 +100,7 @@ func Load(filename string, input io.Reader) (Group, error) { ...@@ -101,14 +100,7 @@ func Load(filename string, input io.Reader) (Group, error) {
// restore logging settings // restore logging settings
log.SetFlags(flags) log.SetFlags(flags)
// secure all the things return configs, nil
configs, err = letsencrypt.Activate(configs)
if err != nil {
return nil, err
}
// group by address/virtualhosts
return arrangeBindings(configs)
} }
// makeOnces makes a map of directive name to sync.Once // makeOnces makes a map of directive name to sync.Once
...@@ -271,12 +263,6 @@ func NewDefault() server.Config { ...@@ -271,12 +263,6 @@ func NewDefault() server.Config {
} }
} }
// Default obtains a default config and arranges
// bindings so it's ready to use.
func Default() (Group, error) {
return arrangeBindings([]server.Config{NewDefault()})
}
// These defaults are configurable through the command line // These defaults are configurable through the command line
var ( var (
// Site root // Site root
......
...@@ -36,6 +36,9 @@ var OnRenew func() error ...@@ -36,6 +36,9 @@ var OnRenew func() error
// argument. If absent, it will use the most recent email // argument. If absent, it will use the most recent email
// address from last time. If there isn't one, the user // address from last time. If there isn't one, the user
// will be prompted. If the user leaves email blank, <TODO>. // will be prompted. If the user leaves email blank, <TODO>.
//
// Also note that calling this function activates asset
// management automatically, which <TODO>.
func Activate(configs []server.Config) ([]server.Config, error) { func Activate(configs []server.Config) ([]server.Config, error) {
// First identify and configure any elligible hosts for which // First identify and configure any elligible hosts for which
// we already have certs and keys in storage from last time. // we already have certs and keys in storage from last time.
...@@ -47,7 +50,7 @@ func Activate(configs []server.Config) ([]server.Config, error) { ...@@ -47,7 +50,7 @@ func Activate(configs []server.Config) ([]server.Config, error) {
} }
// First renew any existing certificates that need it // First renew any existing certificates that need it
processCertificateRenewal(configs) renewCertificates(configs)
// Group configs by LE email address; this will help us // Group configs by LE email address; this will help us
// reduce round-trips when getting the certs. // reduce round-trips when getting the certs.
...@@ -83,11 +86,26 @@ func Activate(configs []server.Config) ([]server.Config, error) { ...@@ -83,11 +86,26 @@ func Activate(configs []server.Config) ([]server.Config, error) {
} }
} }
go keepCertificatesRenewed(configs) stopChan = make(chan struct{})
go maintainAssets(configs, stopChan)
return configs, nil return configs, nil
} }
// Deactivate cleans up long-term, in-memory resources
// allocated by calling Activate(). Essentially, it stops
// the asset maintainer from running, meaning that certificates
// will not be renewed, OCSP staples will not be updated, etc.
func Deactivate() (err error) {
defer func() {
if rec := recover(); rec != nil {
err = errors.New("already deactivated")
}
}()
close(stopChan)
return
}
// groupConfigsByEmail groups configs by the Let's Encrypt email address // groupConfigsByEmail groups configs by the Let's Encrypt email address
// associated to them or to the default Let's Encrypt email address. If the // associated to them or to the default Let's Encrypt email address. If the
// default email is not available, the user will be prompted to provide one. // default email is not available, the user will be prompted to provide one.
...@@ -360,6 +378,9 @@ const ( ...@@ -360,6 +378,9 @@ const (
// How often to check certificates for renewal // How often to check certificates for renewal
renewInterval = 24 * time.Hour renewInterval = 24 * time.Hour
// How often to update OCSP stapling
ocspInterval = 1 * time.Hour
) )
// KeySize represents the length of a key in bits. // KeySize represents the length of a key in bits.
...@@ -377,3 +398,7 @@ const ( ...@@ -377,3 +398,7 @@ const (
// This shouldn't need to change except for in tests; // This shouldn't need to change except for in tests;
// the size can be drastically reduced for speed. // the size can be drastically reduced for speed.
var rsaKeySizeToUse = RSA_2048 var rsaKeySizeToUse = RSA_2048
// stopChan is used to signal the maintenance goroutine
// to terminate.
var stopChan chan struct{}
...@@ -10,33 +10,49 @@ import ( ...@@ -10,33 +10,49 @@ import (
"github.com/xenolf/lego/acme" "github.com/xenolf/lego/acme"
) )
// keepCertificatesRenewed is a permanently-blocking function // maintainAssets is a permanently-blocking function
// that loops indefinitely and, on a regular schedule, checks // that loops indefinitely and, on a regular schedule, checks
// certificates for expiration and initiates a renewal of certs // certificates for expiration and initiates a renewal of certs
// that are expiring soon. // that are expiring soon. It also updates OCSP stapling and
func keepCertificatesRenewed(configs []server.Config) { // performs other maintenance of assets.
ticker := time.Tick(renewInterval) //
for range ticker { // You must pass in the server configs to maintain and the channel
if n, errs := processCertificateRenewal(configs); len(errs) > 0 { // which you'll close when maintenance should stop, to allow this
for _, err := range errs { // goroutine to clean up after itself.
log.Printf("[ERROR] cert renewal: %v\n", err) func maintainAssets(configs []server.Config, stopChan chan struct{}) {
} renewalTicker := time.NewTicker(renewInterval)
if n > 0 && OnRenew != nil { ocspTicker := time.NewTicker(ocspInterval)
err := OnRenew()
if err != nil { for {
log.Printf("[ERROR] onrenew callback: %v\n", err) select {
case <-renewalTicker.C:
if n, errs := renewCertificates(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)
}
} }
} }
case <-ocspTicker.C:
// TODO: Update OCSP
case <-stopChan:
renewalTicker.Stop()
ocspTicker.Stop()
return
} }
} }
} }
// checkCertificateRenewal loops through all configured // renewCertificates loops through all configured site and
// sites and looks for certificates to renew. Nothing is mutated // looks for certificates to renew. Nothing is mutated
// through this function. The changes happen directly on disk. // through this function. The changes happen directly on disk.
// It returns the number of certificates renewed and any errors // It returns the number of certificates renewed and any errors
// that occurred. // that occurred. It only performs a renewal if necessary.
func processCertificateRenewal(configs []server.Config) (int, []error) { func renewCertificates(configs []server.Config) (int, []error) {
log.Print("[INFO] Processing certificate renewals...") log.Print("[INFO] Processing certificate renewals...")
var errs []error var errs []error
var n int var n int
......
...@@ -81,7 +81,7 @@ func Restart(newCaddyfile Input) error { ...@@ -81,7 +81,7 @@ func Restart(newCaddyfile Input) error {
wpipe.Close() wpipe.Close()
// Wait for child process to signal success or fail // Wait for child process to signal success or fail
sigwpipe.Close() // close our copy of the write end of the pipe sigwpipe.Close() // close our copy of the write end of the pipe or we might be stuck
answer, err := ioutil.ReadAll(sigrpipe) answer, err := ioutil.ReadAll(sigrpipe)
if err != nil || len(answer) == 0 { if err != nil || len(answer) == 0 {
log.Println("restart: child failed to answer; changes not applied") log.Println("restart: child failed to answer; changes not applied")
......
...@@ -66,6 +66,7 @@ func New(addr string, configs []Config) (*Server, error) { ...@@ -66,6 +66,7 @@ func New(addr string, configs []Config) (*Server, error) {
// into sync.WaitGroup.Wait() - basically, an add // into sync.WaitGroup.Wait() - basically, an add
// with a positive delta must be guaranteed to // with a positive delta must be guaranteed to
// occur before Wait() is called on the wg. // occur before Wait() is called on the wg.
// In a way, this kind of acts as a safety barrier.
s.httpWg.Add(1) s.httpWg.Add(1)
// Set up each virtualhost // Set up each virtualhost
...@@ -228,12 +229,12 @@ func (s *Server) Stop() error { ...@@ -228,12 +229,12 @@ func (s *Server) Stop() error {
// Wait for remaining connections to finish or // Wait for remaining connections to finish or
// force them all to close after timeout // force them all to close after timeout
select { select {
case <-time.After(5 * time.Second): // TODO: configurable? case <-time.After(5 * time.Second): // TODO: make configurable?
case <-done: case <-done:
} }
} }
// Close the listener now; this stops the server and // Close the listener now; this stops the server without delay
s.listenerMu.Lock() s.listenerMu.Lock()
err := s.listener.Close() err := s.listener.Close()
s.listenerMu.Unlock() s.listenerMu.Unlock()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment