Commit 8811853f authored by Matthew Holt's avatar Matthew Holt

caddytls: Better handle FileStorage and cleaning up locks on exit

parent b7028b13
...@@ -469,9 +469,10 @@ func (i *Instance) Caddyfile() Input { ...@@ -469,9 +469,10 @@ func (i *Instance) Caddyfile() Input {
// //
// This function blocks until all the servers are listening. // This function blocks until all the servers are listening.
func Start(cdyfile Input) (*Instance, error) { func Start(cdyfile Input) (*Instance, error) {
// set up the clustering plugin, if there is one (this should be done // set up the clustering plugin, if there is one (and there should
// exactly once -- but we can't do it during init when they're still // always be one) -- this should be done exactly once, but we can't
// getting plugged in, so do it when starting the first instance) // do it during init while plugins are still registering, so do it
// when starting the first instance)
if atomic.CompareAndSwapInt32(&clusterPluginSetup, 0, 1) { if atomic.CompareAndSwapInt32(&clusterPluginSetup, 0, 1) {
clusterPluginName := os.Getenv("CADDY_CLUSTERING") clusterPluginName := os.Getenv("CADDY_CLUSTERING")
if clusterPluginName == "" { if clusterPluginName == "" {
...@@ -486,6 +487,7 @@ func Start(cdyfile Input) (*Instance, error) { ...@@ -486,6 +487,7 @@ func Start(cdyfile Input) (*Instance, error) {
return nil, fmt.Errorf("constructing cluster plugin %s: %v", clusterPluginName, err) return nil, fmt.Errorf("constructing cluster plugin %s: %v", clusterPluginName, err)
} }
certmagic.DefaultStorage = storage certmagic.DefaultStorage = storage
OnProcessExit = append(OnProcessExit, certmagic.DefaultStorage.UnlockAllObtained)
} }
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})} inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
......
...@@ -98,7 +98,7 @@ func NewConfig(inst *caddy.Instance) *Config { ...@@ -98,7 +98,7 @@ func NewConfig(inst *caddy.Instance) *Config {
certCache, ok := inst.Storage[CertCacheInstStorageKey].(*certmagic.Cache) certCache, ok := inst.Storage[CertCacheInstStorageKey].(*certmagic.Cache)
inst.StorageMu.RUnlock() inst.StorageMu.RUnlock()
if !ok || certCache == nil { if !ok || certCache == nil {
certCache = certmagic.NewCache(certmagic.FileStorage{Path: caddy.AssetsPath()}) certCache = certmagic.NewCache(certmagic.DefaultStorage)
inst.OnShutdown = append(inst.OnShutdown, func() error { inst.OnShutdown = append(inst.OnShutdown, func() error {
certCache.Stop() certCache.Stop()
return nil return nil
...@@ -108,7 +108,7 @@ func NewConfig(inst *caddy.Instance) *Config { ...@@ -108,7 +108,7 @@ func NewConfig(inst *caddy.Instance) *Config {
inst.StorageMu.Unlock() inst.StorageMu.Unlock()
} }
return &Config{ return &Config{
Manager: certmagic.NewWithCache(certCache, certmagic.Config{}), // TODO Manager: certmagic.NewWithCache(certCache, certmagic.Config{}),
} }
} }
......
...@@ -57,7 +57,7 @@ func setupTLS(c *caddy.Controller) error { ...@@ -57,7 +57,7 @@ func setupTLS(c *caddy.Controller) error {
// a single certificate cache is used by the whole caddy.Instance; get a pointer to it // a single certificate cache is used by the whole caddy.Instance; get a pointer to it
certCache, ok := c.Get(CertCacheInstStorageKey).(*certmagic.Cache) certCache, ok := c.Get(CertCacheInstStorageKey).(*certmagic.Cache)
if !ok || certCache == nil { if !ok || certCache == nil {
certCache = certmagic.NewCache(certmagic.FileStorage{Path: caddy.AssetsPath()}) certCache = certmagic.NewCache(certmagic.DefaultStorage)
c.OnShutdown(func() error { c.OnShutdown(func() error {
certCache.Stop() certCache.Stop()
return nil return nil
...@@ -252,18 +252,6 @@ func setupTLS(c *caddy.Controller) error { ...@@ -252,18 +252,6 @@ func setupTLS(c *caddy.Controller) error {
return c.Errf("Setting up DNS provider '%s': %v", dnsProvName, err) return c.Errf("Setting up DNS provider '%s': %v", dnsProvName, err)
} }
config.Manager.DNSProvider = dnsProv config.Manager.DNSProvider = dnsProv
// TODO
// case "storage":
// args := c.RemainingArgs()
// if len(args) != 1 {
// return c.ArgErr()
// }
// storageProvName := args[0]
// storageProvConstr, ok := storageProviders[storageProvName]
// if !ok {
// return c.Errf("Unsupported Storage provider '%s'", args[0])
// }
// config.Manager.Storage = storageProvConstr
case "alpn": case "alpn":
args := c.RemainingArgs() args := c.RemainingArgs()
if len(args) == 0 { if len(args) == 0 {
......
...@@ -165,6 +165,5 @@ func (certCache *Cache) reloadManagedCertificate(oldCert Certificate) error { ...@@ -165,6 +165,5 @@ func (certCache *Cache) reloadManagedCertificate(oldCert Certificate) error {
return nil return nil
} }
// defaultCache is a convenient, default certificate cache for var defaultCache *Cache
// use by this process when no other certificate cache is provided. var defaultCacheMu sync.Mutex
var defaultCache = NewCache(DefaultStorage)
...@@ -94,7 +94,7 @@ func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) { ...@@ -94,7 +94,7 @@ func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) {
legoCfg := lego.NewConfig(&leUser) legoCfg := lego.NewConfig(&leUser)
legoCfg.CADirURL = caURL legoCfg.CADirURL = caURL
legoCfg.KeyType = keyType legoCfg.KeyType = keyType
legoCfg.UserAgent = UserAgent legoCfg.UserAgent = buildUAString()
legoCfg.HTTPClient.Timeout = HTTPTimeout legoCfg.HTTPClient.Timeout = HTTPTimeout
client, err = lego.NewClient(legoCfg) client, err = lego.NewClient(legoCfg)
if err != nil { if err != nil {
...@@ -373,6 +373,14 @@ func (c *acmeClient) Revoke(name string) error { ...@@ -373,6 +373,14 @@ func (c *acmeClient) Revoke(name string) error {
return nil return nil
} }
func buildUAString() string {
ua := "CertMagic"
if UserAgent != "" {
ua += " " + UserAgent
}
return ua
}
// Some default values passed down to the underlying lego client. // Some default values passed down to the underlying lego client.
var ( var (
UserAgent string UserAgent string
......
...@@ -125,15 +125,28 @@ func NewDefault() *Config { ...@@ -125,15 +125,28 @@ func NewDefault() *Config {
// a default certificate cache. All calls to // a default certificate cache. All calls to
// New() will use the same certificate cache. // New() will use the same certificate cache.
func New(cfg Config) *Config { func New(cfg Config) *Config {
return NewWithCache(defaultCache, cfg) return NewWithCache(nil, cfg)
} }
// NewWithCache makes a valid new config based on cfg // NewWithCache makes a valid new config based on cfg
// and uses the provided certificate cache. // and uses the provided certificate cache. If certCache
// is nil, a new, default one will be created using
// DefaultStorage; or, if a default cache has already
// been created, it will be reused.
func NewWithCache(certCache *Cache, cfg Config) *Config { func NewWithCache(certCache *Cache, cfg Config) *Config {
// avoid nil pointers with sensible defaults // avoid nil pointers with sensible defaults,
// careful to initialize a default cache (which
// begins its maintenance goroutine) only if
// needed - and only once (we don't initialize
// it at package init to give importers a chance
// to set DefaultStorage if they so desire)
if certCache == nil { if certCache == nil {
defaultCacheMu.Lock()
if defaultCache == nil {
defaultCache = NewCache(DefaultStorage)
}
certCache = defaultCache certCache = defaultCache
defaultCacheMu.Unlock()
} }
if certCache.storage == nil { if certCache.storage == nil {
certCache.storage = DefaultStorage certCache.storage = DefaultStorage
......
...@@ -17,6 +17,7 @@ package certmagic ...@@ -17,6 +17,7 @@ package certmagic
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
...@@ -171,18 +172,36 @@ func (fs FileStorage) TryLock(key string) (Waiter, error) { ...@@ -171,18 +172,36 @@ func (fs FileStorage) TryLock(key string) (Waiter, error) {
} }
fw = &fileStorageWaiter{ fw = &fileStorageWaiter{
key: key,
filename: filepath.Join(lockDir, StorageKeys.safe(key)+".lock"), filename: filepath.Join(lockDir, StorageKeys.safe(key)+".lock"),
wg: new(sync.WaitGroup), wg: new(sync.WaitGroup),
} }
var checkedStaleLock bool // sentinel value to avoid infinite goto-ing
createLock:
// create the file in a special mode such that an // create the file in a special mode such that an
// error is returned if it already exists // error is returned if it already exists
lf, err := os.OpenFile(fw.filename, os.O_CREATE|os.O_EXCL, 0644) lf, err := os.OpenFile(fw.filename, os.O_CREATE|os.O_EXCL, 0644)
if err != nil { if err != nil {
if os.IsExist(err) { if os.IsExist(err) {
// another process has the lock; use it to wait // another process has the lock
// check to see if the lock is stale, if we haven't already
if !checkedStaleLock {
checkedStaleLock = true
if fs.lockFileStale(fw.filename) {
log.Printf("[INFO][%s] Lock for '%s' is stale; removing then retrying: %s",
fs, key, fw.filename)
os.Remove(fw.filename)
goto createLock
}
}
// if lock is not stale, wait upon it
return fw, nil return fw, nil
} }
// otherwise, this was some unexpected error // otherwise, this was some unexpected error
return nil, err return nil, err
} }
...@@ -225,10 +244,33 @@ func (fs FileStorage) Unlock(key string) error { ...@@ -225,10 +244,33 @@ func (fs FileStorage) Unlock(key string) error {
return nil return nil
} }
// UnlockAllObtained removes all locks obtained by
// this instance of fs.
func (fs FileStorage) UnlockAllObtained() {
for key, fw := range fileStorageNameLocks {
err := fs.Unlock(fw.key)
if err != nil {
log.Printf("[ERROR][%s] Releasing obtained lock for %s: %v", fs, key, err)
}
}
}
func (fs FileStorage) lockFileStale(filename string) bool {
info, err := os.Stat(filename)
if err != nil {
return true // no good way to handle this, really; lock is useless?
}
return time.Since(info.ModTime()) > staleLockDuration
}
func (fs FileStorage) lockDir() string { func (fs FileStorage) lockDir() string {
return filepath.Join(fs.Path, "locks") return filepath.Join(fs.Path, "locks")
} }
func (fs FileStorage) String() string {
return "FileStorage:" + fs.Path
}
// fileStorageWaiter waits for a file to disappear; it // fileStorageWaiter waits for a file to disappear; it
// polls the file system to check for the existence of // polls the file system to check for the existence of
// a file. It also uses a WaitGroup to optimize the // a file. It also uses a WaitGroup to optimize the
...@@ -237,6 +279,7 @@ func (fs FileStorage) lockDir() string { ...@@ -237,6 +279,7 @@ func (fs FileStorage) lockDir() string {
// the lock will still block, but must wait for the // the lock will still block, but must wait for the
// polling to get their answer.) // polling to get their answer.)
type fileStorageWaiter struct { type fileStorageWaiter struct {
key string
filename string filename string
wg *sync.WaitGroup wg *sync.WaitGroup
} }
...@@ -259,3 +302,7 @@ var fileStorageNameLocksMu sync.Mutex ...@@ -259,3 +302,7 @@ var fileStorageNameLocksMu sync.Mutex
var _ Storage = FileStorage{} var _ Storage = FileStorage{}
var _ Waiter = &fileStorageWaiter{} var _ Waiter = &fileStorageWaiter{}
// staleLockDuration is the length of time
// before considering a lock to be stale.
const staleLockDuration = 2 * time.Hour
...@@ -38,21 +38,21 @@ func (certCache *Cache) maintainAssets() { ...@@ -38,21 +38,21 @@ func (certCache *Cache) maintainAssets() {
for { for {
select { select {
case <-renewalTicker.C: case <-renewalTicker.C:
log.Println("[INFO] Scanning for expiring certificates") log.Printf("[INFO][%s] Scanning for expiring certificates", certCache.storage)
err := certCache.RenewManagedCertificates(false) err := certCache.RenewManagedCertificates(false)
if err != nil { if err != nil {
log.Printf("[ERROR] Renewing managed certificates: %v", err) log.Printf("[ERROR][%s] Renewing managed certificates: %v", certCache.storage, err)
} }
log.Println("[INFO] Done checking certificates") log.Printf("[INFO][%s] Done scanning certificates", certCache.storage)
case <-ocspTicker.C: case <-ocspTicker.C:
log.Println("[INFO] Scanning for stale OCSP staples") log.Printf("[INFO][%s] Scanning for stale OCSP staples", certCache.storage)
certCache.updateOCSPStaples() certCache.updateOCSPStaples()
certCache.deleteOldStapleFiles() certCache.deleteOldStapleFiles()
log.Println("[INFO] Done checking OCSP staples") log.Printf("[INFO][%s] Done checking OCSP staples", certCache.storage)
case <-certCache.stopChan: case <-certCache.stopChan:
renewalTicker.Stop() renewalTicker.Stop()
ocspTicker.Stop() ocspTicker.Stop()
log.Println("[INFO] Stopped certificate maintenance routine") log.Printf("[INFO][%s] Stopped certificate maintenance routine", certCache.storage)
return return
} }
} }
......
...@@ -30,6 +30,8 @@ import ( ...@@ -30,6 +30,8 @@ import (
// same Storage value (its implementation and configuration) // same Storage value (its implementation and configuration)
// in order to share certificates and other TLS resources // in order to share certificates and other TLS resources
// with the cluster. // with the cluster.
//
// Implementations of Storage must be safe for concurrent use.
type Storage interface { type Storage interface {
// Locker provides atomic synchronization // Locker provides atomic synchronization
// operations, making Storage safe to share. // operations, making Storage safe to share.
...@@ -87,6 +89,15 @@ type Locker interface { ...@@ -87,6 +89,15 @@ type Locker interface {
// TryLock or if Unlock was not called at all. Unlock should also // TryLock or if Unlock was not called at all. Unlock should also
// clean up any unused resources allocated during TryLock. // clean up any unused resources allocated during TryLock.
Unlock(key string) error Unlock(key string) error
// UnlockAllObtained removes all locks obtained by this process,
// upon which others may be waiting. The importer should call
// this on shutdowns (and crashes, ideally) to avoid leaving stale
// locks, but Locker implementations must NOT rely on this being
// the case and should anticipate and handle stale locks. Errors
// should be printed or logged, since there could be multiple,
// with no good way to handle them anyway.
UnlockAllObtained()
} }
// Waiter is a type that can block until a lock is released. // Waiter is a type that can block until a lock is released.
......
...@@ -138,7 +138,7 @@ ...@@ -138,7 +138,7 @@
"importpath": "github.com/mholt/certmagic", "importpath": "github.com/mholt/certmagic",
"repository": "https://github.com/mholt/certmagic", "repository": "https://github.com/mholt/certmagic",
"vcs": "git", "vcs": "git",
"revision": "dc98c40439d15f67021f10f0d9219a39d7cf2990", "revision": "fe722057f2654b33cd528b8fd8b90e53fa495564",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },
......
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