Commit 0b83014f authored by Matthew Holt's avatar Matthew Holt

caddytls: Use latest certmagic package, with updated Storage interface

parent 0684cf86
...@@ -487,7 +487,6 @@ func Start(cdyfile Input) (*Instance, error) { ...@@ -487,7 +487,6 @@ 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{})}
......
...@@ -432,5 +432,5 @@ func loadCertsInDir(cfg *Config, c *caddy.Controller, dir string) error { ...@@ -432,5 +432,5 @@ func loadCertsInDir(cfg *Config, c *caddy.Controller, dir string) error {
} }
func constructDefaultClusterPlugin() (certmagic.Storage, error) { func constructDefaultClusterPlugin() (certmagic.Storage, error) {
return certmagic.FileStorage{Path: caddy.AssetsPath()}, nil return &certmagic.FileStorage{Path: caddy.AssetsPath()}, nil
} }
...@@ -64,12 +64,11 @@ func (c Certificate) NeedsRenewal() bool { ...@@ -64,12 +64,11 @@ func (c Certificate) NeedsRenewal() bool {
if c.NotAfter.IsZero() { if c.NotAfter.IsZero() {
return false return false
} }
timeLeft := c.NotAfter.UTC().Sub(time.Now().UTC())
renewDurationBefore := DefaultRenewDurationBefore renewDurationBefore := DefaultRenewDurationBefore
if len(c.configs) > 0 && c.configs[0].RenewDurationBefore > 0 { if len(c.configs) > 0 && c.configs[0].RenewDurationBefore > 0 {
renewDurationBefore = c.configs[0].RenewDurationBefore renewDurationBefore = c.configs[0].RenewDurationBefore
} }
return timeLeft < renewDurationBefore return time.Until(c.NotAfter) < renewDurationBefore
} }
// CacheManagedCertificate loads the certificate for domain into the // CacheManagedCertificate loads the certificate for domain into the
......
...@@ -52,6 +52,16 @@ import ( ...@@ -52,6 +52,16 @@ import (
// HTTPS serves mux for all domainNames using the HTTP // HTTPS serves mux for all domainNames using the HTTP
// and HTTPS ports, redirecting all HTTP requests to HTTPS. // and HTTPS ports, redirecting all HTTP requests to HTTPS.
// //
// This high-level convenience function is opinionated and
// applies sane defaults for production use, including
// timeouts for HTTP requests and responses. To allow very
// long-lived requests or connections, you should make your
// own http.Server values and use this package's Listen(),
// TLS(), or Config.TLSConfig() functions to customize to
// your needs. For example, servers which need to support
// large uploads or downloads with slow clients may need to
// use longer timeouts, thus this function is not suitable.
//
// Calling this function signifies your acceptance to // Calling this function signifies your acceptance to
// the CA's Subscriber Agreement and/or Terms of Service. // the CA's Subscriber Agreement and/or Terms of Service.
func HTTPS(domainNames []string, mux http.Handler) error { func HTTPS(domainNames []string, mux http.Handler) error {
...@@ -96,13 +106,32 @@ func HTTPS(domainNames []string, mux http.Handler) error { ...@@ -96,13 +106,32 @@ func HTTPS(domainNames []string, mux http.Handler) error {
hln, hsln := httpLn, httpsLn hln, hsln := httpLn, httpsLn
lnMu.Unlock() lnMu.Unlock()
httpHandler := cfg.HTTPChallengeHandler(http.HandlerFunc(httpRedirectHandler)) // create HTTP/S servers that are configured
// with sane default timeouts and appropriate
// handlers (the HTTP server solves the HTTP
// challenge and issues redirects to HTTPS,
// while the HTTPS server simply serves the
// user's handler)
httpServer := &http.Server{
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 5 * time.Second,
Handler: cfg.HTTPChallengeHandler(http.HandlerFunc(httpRedirectHandler)),
}
httpsServer := &http.Server{
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 2 * time.Minute,
IdleTimeout: 5 * time.Minute,
Handler: mux,
}
log.Printf("%v Serving HTTP->HTTPS on %s and %s", log.Printf("%v Serving HTTP->HTTPS on %s and %s",
domainNames, hln.Addr(), hsln.Addr()) domainNames, hln.Addr(), hsln.Addr())
go http.Serve(hln, httpHandler) go httpServer.Serve(hln)
return http.Serve(hsln, mux) return httpsServer.Serve(hsln)
} }
func httpRedirectHandler(w http.ResponseWriter, r *http.Request) { func httpRedirectHandler(w http.ResponseWriter, r *http.Request) {
......
...@@ -208,6 +208,8 @@ func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) { ...@@ -208,6 +208,8 @@ func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) {
return c, nil return c, nil
} }
// lockKey returns a key for a lock that is specific to the operation
// named op being performed related to domainName and this config's CA.
func (cfg *Config) lockKey(op, domainName string) string { func (cfg *Config) lockKey(op, domainName string) string {
return fmt.Sprintf("%s:%s:%s", op, domainName, cfg.CA) return fmt.Sprintf("%s:%s:%s", op, domainName, cfg.CA)
} }
...@@ -215,30 +217,34 @@ func (cfg *Config) lockKey(op, domainName string) string { ...@@ -215,30 +217,34 @@ func (cfg *Config) lockKey(op, domainName string) string {
// Obtain obtains a single certificate for name. It stores the certificate // Obtain obtains a single certificate for name. It stores the certificate
// on the disk if successful. This function is safe for concurrent use. // on the disk if successful. This function is safe for concurrent use.
// //
// Right now our storage mechanism only supports one name per certificate, // Our storage mechanism only supports one name per certificate, so this
// so this function (along with Renew and Revoke) only accepts one domain // function (along with Renew and Revoke) only accepts one domain as input.
// as input. It can be easily modified to support SAN certificates if our // It could be easily modified to support SAN certificates if our storage
// storage mechanism is upgraded later. // mechanism is upgraded later, but that will increase logical complexity
// in other areas.
// //
// Callers who have access to a Config value should use the ObtainCert // Callers who have access to a Config value should use the ObtainCert
// method on that instead of this lower-level method. // method on that instead of this lower-level method.
func (c *acmeClient) Obtain(name string) error { func (c *acmeClient) Obtain(name string) error {
// ensure idempotency of the obtain operation for this name
lockKey := c.config.lockKey("cert_acme", name) lockKey := c.config.lockKey("cert_acme", name)
waiter, err := c.config.certCache.storage.TryLock(lockKey) err := c.config.certCache.storage.Lock(lockKey)
if err != nil { if err != nil {
return err return err
} }
if waiter != nil {
log.Printf("[INFO] Certificate for %s is already being obtained elsewhere and stored; waiting", name)
waiter.Wait()
return nil // we assume the process with the lock succeeded, rather than hammering this execution path again
}
defer func() { defer func() {
if err := c.config.certCache.storage.Unlock(lockKey); err != nil { if err := c.config.certCache.storage.Unlock(lockKey); err != nil {
log.Printf("[ERROR] Unable to unlock obtain call for %s: %v", name, err) log.Printf("[ERROR][%s] Obtain: Unable to unlock '%s': %v", name, lockKey, err)
} }
}() }()
// check if obtain is still needed -- might have
// been obtained during lock
if c.config.storageHasCertResources(name) {
log.Printf("[INFO][%s] Obtain: Certificate already exists in storage", name)
return nil
}
for attempts := 0; attempts < 2; attempts++ { for attempts := 0; attempts < 2; attempts++ {
request := certificate.ObtainRequest{ request := certificate.ObtainRequest{
Domains: []string{name}, Domains: []string{name},
...@@ -280,19 +286,15 @@ func (c *acmeClient) Obtain(name string) error { ...@@ -280,19 +286,15 @@ func (c *acmeClient) Obtain(name string) error {
// Callers who have access to a Config value should use the RenewCert // Callers who have access to a Config value should use the RenewCert
// method on that instead of this lower-level method. // method on that instead of this lower-level method.
func (c *acmeClient) Renew(name string) error { func (c *acmeClient) Renew(name string) error {
// ensure idempotency of the renew operation for this name
lockKey := c.config.lockKey("cert_acme", name) lockKey := c.config.lockKey("cert_acme", name)
waiter, err := c.config.certCache.storage.TryLock(lockKey) err := c.config.certCache.storage.Lock(lockKey)
if err != nil { if err != nil {
return err return err
} }
if waiter != nil {
log.Printf("[INFO] Certificate for %s is already being renewed elsewhere and stored; waiting", name)
waiter.Wait()
return nil // assume that the worker that renewed the cert succeeded to avoid hammering this path over and over
}
defer func() { defer func() {
if err := c.config.certCache.storage.Unlock(lockKey); err != nil { if err := c.config.certCache.storage.Unlock(lockKey); err != nil {
log.Printf("[ERROR] Unable to unlock renew call for %s: %v", name, err) log.Printf("[ERROR][%s] Renew: Unable to unlock '%s': %v", name, lockKey, err)
} }
}() }()
...@@ -302,6 +304,12 @@ func (c *acmeClient) Renew(name string) error { ...@@ -302,6 +304,12 @@ func (c *acmeClient) Renew(name string) error {
return err return err
} }
// Check if renew is still needed - might have been renewed while waiting for lock
if !c.config.managedCertNeedsRenewal(certRes) {
log.Printf("[INFO][%s] Renew: Certificate appears to have been renewed already", name)
return nil
}
// Perform renewal and retry if necessary, but not too many times. // Perform renewal and retry if necessary, but not too many times.
var newCertMeta *certificate.Resource var newCertMeta *certificate.Resource
var success bool var success bool
......
...@@ -21,6 +21,7 @@ import ( ...@@ -21,6 +21,7 @@ import (
"time" "time"
"github.com/xenolf/lego/certcrypto" "github.com/xenolf/lego/certcrypto"
"github.com/xenolf/lego/certificate"
"github.com/xenolf/lego/challenge" "github.com/xenolf/lego/challenge"
"github.com/xenolf/lego/challenge/tlsalpn01" "github.com/xenolf/lego/challenge/tlsalpn01"
"github.com/xenolf/lego/lego" "github.com/xenolf/lego/lego"
...@@ -277,12 +278,6 @@ func (cfg *Config) ObtainCert(name string, interactive bool) error { ...@@ -277,12 +278,6 @@ func (cfg *Config) ObtainCert(name string, interactive bool) error {
return nil return nil
} }
// we expect this to be a new site; if the
// cert already exists, then no-op
if cfg.certCache.storage.Exists(StorageKeys.SiteCert(cfg.CA, name)) {
return nil
}
client, err := cfg.newACMEClient(interactive) client, err := cfg.newACMEClient(interactive)
if err != nil { if err != nil {
return err return err
...@@ -317,24 +312,37 @@ func (cfg *Config) RevokeCert(domain string, interactive bool) error { ...@@ -317,24 +312,37 @@ func (cfg *Config) RevokeCert(domain string, interactive bool) error {
return client.Revoke(domain) return client.Revoke(domain)
} }
// TLSConfig returns a TLS configuration that // TLSConfig is an opinionated method that returns a
// can be used to configure TLS listeners. It // recommended, modern TLS configuration that can be
// supports the TLS-ALPN challenge and serves // used to configure TLS listeners, which also supports
// up certificates managed by cfg. // the TLS-ALPN challenge and serves up certificates
// managed by cfg.
//
// Unlike the package TLS() function, this method does
// not, by itself, enable certificate management for
// any domain names.
//
// Feel free to further customize the returned tls.Config,
// but do not mess with the GetCertificate or NextProtos
// fields unless you know what you're doing, as they're
// necessary to solve the TLS-ALPN challenge.
func (cfg *Config) TLSConfig() *tls.Config { func (cfg *Config) TLSConfig() *tls.Config {
return &tls.Config{ return &tls.Config{
// these two fields necessary for TLS-ALPN challenge
GetCertificate: cfg.GetCertificate, GetCertificate: cfg.GetCertificate,
NextProtos: []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol}, NextProtos: []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol},
// the rest recommended for modern TLS servers
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
CipherSuites: preferredDefaultCipherSuites(),
PreferServerCipherSuites: true,
} }
} }
// RenewAllCerts triggers a renewal check of all
// certificates in the cache. It only renews
// certificates if they need to be renewed.
// func (cfg *Config) RenewAllCerts(interactive bool) error {
// return cfg.certCache.RenewManagedCertificates(interactive)
// }
// preObtainOrRenewChecks perform a few simple checks before // preObtainOrRenewChecks perform a few simple checks before
// obtaining or renewing a certificate with ACME, and returns // obtaining or renewing a certificate with ACME, and returns
// whether this name should be skipped (like if it's not // whether this name should be skipped (like if it's not
...@@ -356,3 +364,27 @@ func (cfg *Config) preObtainOrRenewChecks(name string, allowPrompts bool) (bool, ...@@ -356,3 +364,27 @@ func (cfg *Config) preObtainOrRenewChecks(name string, allowPrompts bool) (bool,
return false, nil return false, nil
} }
// storageHasCertResources returns true if the storage
// associated with cfg's certificate cache has all the
// resources related to the certificate for domain: the
// certificate, the private key, and the metadata.
func (cfg *Config) storageHasCertResources(domain string) bool {
certKey := StorageKeys.SiteCert(cfg.CA, domain)
keyKey := StorageKeys.SitePrivateKey(cfg.CA, domain)
metaKey := StorageKeys.SiteMeta(cfg.CA, domain)
return cfg.certCache.storage.Exists(certKey) &&
cfg.certCache.storage.Exists(keyKey) &&
cfg.certCache.storage.Exists(metaKey)
}
// managedCertNeedsRenewal returns true if certRes is
// expiring soon or already expired, or if the process
// of checking the expiration returned an error.
func (cfg *Config) managedCertNeedsRenewal(certRes certificate.Resource) bool {
cert, err := cfg.makeCertificate(certRes.Certificate, certRes.PrivateKey)
if err != nil {
return true
}
return cert.NeedsRenewal()
}
...@@ -19,12 +19,14 @@ import ( ...@@ -19,12 +19,14 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/rsa" "crypto/rsa"
"crypto/sha256" "crypto/sha256"
"crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"hash/fnv" "hash/fnv"
"github.com/klauspost/cpuid"
"github.com/xenolf/lego/certificate" "github.com/xenolf/lego/certificate"
) )
...@@ -153,3 +155,34 @@ func hashCertificateChain(certChain [][]byte) string { ...@@ -153,3 +155,34 @@ func hashCertificateChain(certChain [][]byte) string {
} }
return fmt.Sprintf("%x", h.Sum(nil)) return fmt.Sprintf("%x", h.Sum(nil))
} }
// preferredDefaultCipherSuites returns an appropriate
// cipher suite to use depending on hardware support
// for AES-NI.
//
// See https://github.com/mholt/caddy/issues/1674
func preferredDefaultCipherSuites() []uint16 {
if cpuid.CPU.AesNi() {
return defaultCiphersPreferAES
}
return defaultCiphersPreferChaCha
}
var (
defaultCiphersPreferAES = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
}
defaultCiphersPreferChaCha = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
}
)
...@@ -22,7 +22,6 @@ import ( ...@@ -22,7 +22,6 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sync"
"time" "time"
) )
...@@ -34,13 +33,13 @@ type FileStorage struct { ...@@ -34,13 +33,13 @@ type FileStorage struct {
} }
// Exists returns true if key exists in fs. // Exists returns true if key exists in fs.
func (fs FileStorage) Exists(key string) bool { func (fs *FileStorage) Exists(key string) bool {
_, err := os.Stat(fs.Filename(key)) _, err := os.Stat(fs.Filename(key))
return !os.IsNotExist(err) return !os.IsNotExist(err)
} }
// Store saves value at key. // Store saves value at key.
func (fs FileStorage) Store(key string, value []byte) error { func (fs *FileStorage) Store(key string, value []byte) error {
filename := fs.Filename(key) filename := fs.Filename(key)
err := os.MkdirAll(filepath.Dir(filename), 0700) err := os.MkdirAll(filepath.Dir(filename), 0700)
if err != nil { if err != nil {
...@@ -50,7 +49,7 @@ func (fs FileStorage) Store(key string, value []byte) error { ...@@ -50,7 +49,7 @@ func (fs FileStorage) Store(key string, value []byte) error {
} }
// Load retrieves the value at key. // Load retrieves the value at key.
func (fs FileStorage) Load(key string) ([]byte, error) { func (fs *FileStorage) Load(key string) ([]byte, error) {
contents, err := ioutil.ReadFile(fs.Filename(key)) contents, err := ioutil.ReadFile(fs.Filename(key))
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil, ErrNotExist(err) return nil, ErrNotExist(err)
...@@ -59,8 +58,7 @@ func (fs FileStorage) Load(key string) ([]byte, error) { ...@@ -59,8 +58,7 @@ func (fs FileStorage) Load(key string) ([]byte, error) {
} }
// Delete deletes the value at key. // Delete deletes the value at key.
// TODO: Delete any empty folders caused by this operation func (fs *FileStorage) Delete(key string) error {
func (fs FileStorage) Delete(key string) error {
err := os.Remove(fs.Filename(key)) err := os.Remove(fs.Filename(key))
if os.IsNotExist(err) { if os.IsNotExist(err) {
return ErrNotExist(err) return ErrNotExist(err)
...@@ -69,7 +67,7 @@ func (fs FileStorage) Delete(key string) error { ...@@ -69,7 +67,7 @@ func (fs FileStorage) Delete(key string) error {
} }
// List returns all keys that match prefix. // List returns all keys that match prefix.
func (fs FileStorage) List(prefix string, recursive bool) ([]string, error) { func (fs *FileStorage) List(prefix string, recursive bool) ([]string, error) {
var keys []string var keys []string
walkPrefix := fs.Filename(prefix) walkPrefix := fs.Filename(prefix)
...@@ -100,7 +98,7 @@ func (fs FileStorage) List(prefix string, recursive bool) ([]string, error) { ...@@ -100,7 +98,7 @@ func (fs FileStorage) List(prefix string, recursive bool) ([]string, error) {
} }
// Stat returns information about key. // Stat returns information about key.
func (fs FileStorage) Stat(key string) (KeyInfo, error) { func (fs *FileStorage) Stat(key string) (KeyInfo, error) {
fi, err := os.Stat(fs.Filename(key)) fi, err := os.Stat(fs.Filename(key))
if os.IsNotExist(err) { if os.IsNotExist(err) {
return KeyInfo{}, ErrNotExist(err) return KeyInfo{}, ErrNotExist(err)
...@@ -118,191 +116,160 @@ func (fs FileStorage) Stat(key string) (KeyInfo, error) { ...@@ -118,191 +116,160 @@ func (fs FileStorage) Stat(key string) (KeyInfo, error) {
// Filename returns the key as a path on the file // Filename returns the key as a path on the file
// system prefixed by fs.Path. // system prefixed by fs.Path.
func (fs FileStorage) Filename(key string) string { func (fs *FileStorage) Filename(key string) string {
return filepath.Join(fs.Path, filepath.FromSlash(key)) return filepath.Join(fs.Path, filepath.FromSlash(key))
} }
// homeDir returns the best guess of the current user's home // Lock obtains a lock named by the given key. It blocks
// directory from environment variables. If unknown, "." (the // until the lock can be obtained or an error is returned.
// current directory) is returned instead. func (fs *FileStorage) Lock(key string) error {
func homeDir() string { start := time.Now()
home := os.Getenv("HOME") filename := fs.lockFilename(key)
if home == "" && runtime.GOOS == "windows" {
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
home = drive + path
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
}
if home == "" {
home = "."
}
return home
}
func dataDir() string {
baseDir := filepath.Join(homeDir(), ".local", "share")
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
baseDir = xdgData
}
return filepath.Join(baseDir, "certmagic")
}
// TryLock attempts to get a lock for name, otherwise it returns for {
// a Waiter value to wait until the other process is finished. err := createLockfile(filename)
func (fs FileStorage) TryLock(key string) (Waiter, error) { if err == nil {
fileStorageNameLocksMu.Lock() // got the lock, yay
defer fileStorageNameLocksMu.Unlock() return nil
// see if lock already exists within this process - allows
// for faster unlocking since we don't have to poll the disk
fw, ok := fileStorageNameLocks[key]
if ok {
// lock already created within process, let caller wait on it
return fw, nil
} }
if !os.IsExist(err) {
// attempt to persist lock to disk by creating lock file // unexpected error
return fmt.Errorf("creating lock file: %v", err)
// parent dir must exist
lockDir := fs.lockDir()
if err := os.MkdirAll(lockDir, 0700); err != nil {
return nil, err
} }
fw = &fileStorageWaiter{ // lock file already exists
key: key,
filename: filepath.Join(lockDir, StorageKeys.safe(key)+".lock"),
wg: new(sync.WaitGroup),
}
var checkedStaleLock bool // sentinel value to avoid infinite goto-ing info, err := os.Stat(filename)
switch {
case os.IsNotExist(err):
// must have just been removed; try again to create it
continue
createLock: case err != nil:
// create the file in a special mode such that an // unexpected error
// error is returned if it already exists return fmt.Errorf("accessing lock file: %v", err)
lf, err := os.OpenFile(fw.filename, os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
if os.IsExist(err) {
// another process has the lock
// check to see if the lock is stale, if we haven't already case fileLockIsStale(info):
if !checkedStaleLock { // lock file is stale - delete it and try again to create one
checkedStaleLock = true
if fs.lockFileStale(fw.filename) {
log.Printf("[INFO][%s] Lock for '%s' is stale; removing then retrying: %s", log.Printf("[INFO][%s] Lock for '%s' is stale; removing then retrying: %s",
fs, key, fw.filename) fs, key, filename)
os.Remove(fw.filename) removeLockfile(filename)
goto createLock continue
case time.Since(start) > staleLockDuration*2:
// should never happen, hopefully
return fmt.Errorf("possible deadlock: %s passed trying to obtain lock for %s",
time.Since(start), key)
default:
// lockfile exists and is not stale;
// just wait a moment and try again
time.Sleep(fileLockPollInterval)
} }
} }
// if lock is not stale, wait upon it
return fw, nil
}
// otherwise, this was some unexpected error
return nil, err
}
lf.Close()
// looks like we get the lock
fw.wg.Add(1)
fileStorageNameLocks[key] = fw
return nil, nil
} }
// Unlock releases the lock for name. // Unlock releases the lock for name.
func (fs FileStorage) Unlock(key string) error { func (fs *FileStorage) Unlock(key string) error {
fileStorageNameLocksMu.Lock() return removeLockfile(fs.lockFilename(key))
defer fileStorageNameLocksMu.Unlock() }
fw, ok := fileStorageNameLocks[key]
if !ok {
return fmt.Errorf("FileStorage: no lock to release for %s", key)
}
// remove lock file
os.Remove(fw.filename)
// if parent folder is now empty, remove it too to keep it tidy
dir, err := os.Open(fs.lockDir()) // OK to ignore error here
if err == nil {
items, _ := dir.Readdirnames(3) // OK to ignore error here
if len(items) == 0 {
os.Remove(dir.Name())
}
dir.Close()
}
// clean up in memory func (fs *FileStorage) String() string {
fw.wg.Done() return "FileStorage:" + fs.Path
delete(fileStorageNameLocks, key) }
return nil func (fs *FileStorage) lockFilename(key string) string {
return filepath.Join(fs.lockDir(), StorageKeys.safe(key)+".lock")
} }
// UnlockAllObtained removes all locks obtained by func (fs *FileStorage) lockDir() string {
// this instance of fs. return filepath.Join(fs.Path, "locks")
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 { func fileLockIsStale(info os.FileInfo) bool {
info, err := os.Stat(filename) if info == nil {
if err != nil { return true
return true // no good way to handle this, really; lock is useless?
} }
return time.Since(info.ModTime()) > staleLockDuration return time.Since(info.ModTime()) > staleLockDuration
} }
func (fs FileStorage) lockDir() string { // createLockfile atomically creates the lockfile
return filepath.Join(fs.Path, "locks") // identified by filename. A successfully created
// lockfile should be removed with removeLockfile.
func createLockfile(filename string) error {
err := atomicallyCreateFile(filename)
if err == nil {
// if the app crashes in removeLockfile(), there is a
// small chance the .unlock file is left behind; it's
// safe to simply remove it as it's a guard against
// double removal of the .lock file.
os.Remove(filename + ".unlock")
}
return err
} }
func (fs FileStorage) String() string { // removeLockfile atomically removes filename,
return "FileStorage:" + fs.Path // which must be a lockfile created by createLockfile.
// See discussion in PR #7 for more background:
// https://github.com/mholt/certmagic/pull/7
func removeLockfile(filename string) error {
unlockFilename := filename + ".unlock"
if err := atomicallyCreateFile(unlockFilename); err != nil {
if os.IsExist(err) {
// another process is handling the unlocking
return nil
}
return err
}
defer os.Remove(unlockFilename)
return os.Remove(filename)
} }
// fileStorageWaiter waits for a file to disappear; it // atomicallyCreateFile atomically creates the file
// polls the file system to check for the existence of // identified by filename if it doesn't already exist.
// a file. It also uses a WaitGroup to optimize the func atomicallyCreateFile(filename string) error {
// polling in the case when this process is the only // no need to check this, we only really care about the file creation error
// one waiting. (Other processes that are waiting for os.MkdirAll(filepath.Dir(filename), 0700)
// the lock will still block, but must wait for the f, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL, 0644)
// polling to get their answer.) if err == nil {
type fileStorageWaiter struct { f.Close()
key string }
filename string return err
wg *sync.WaitGroup
} }
// Wait waits until the lock is released. // homeDir returns the best guess of the current user's home
func (fw *fileStorageWaiter) Wait() { // directory from environment variables. If unknown, "." (the
start := time.Now() // current directory) is returned instead.
fw.wg.Wait() func homeDir() string {
for time.Since(start) < 1*time.Hour { home := os.Getenv("HOME")
_, err := os.Stat(fw.filename) if home == "" && runtime.GOOS == "windows" {
if os.IsNotExist(err) { drive := os.Getenv("HOMEDRIVE")
return path := os.Getenv("HOMEPATH")
home = drive + path
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
} }
time.Sleep(1 * time.Second) if home == "" {
home = "."
} }
return home
} }
var fileStorageNameLocks = make(map[string]*fileStorageWaiter) func dataDir() string {
var fileStorageNameLocksMu sync.Mutex baseDir := filepath.Join(homeDir(), ".local", "share")
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
var _ Storage = FileStorage{} baseDir = xdgData
var _ Waiter = &fileStorageWaiter{} }
return filepath.Join(baseDir, "certmagic")
}
// staleLockDuration is the length of time // staleLockDuration is the length of time
// before considering a lock to be stale. // before considering a lock to be stale.
const staleLockDuration = 2 * time.Hour const staleLockDuration = 2 * time.Hour
// fileLockPollInterval is how frequently
// to check the existence of a lock file
const fileLockPollInterval = 1 * time.Second
var _ Storage = (*FileStorage)(nil)
...@@ -64,22 +64,24 @@ type Storage interface { ...@@ -64,22 +64,24 @@ type Storage interface {
// Locker facilitates synchronization of certificate tasks across // Locker facilitates synchronization of certificate tasks across
// machines and networks. // machines and networks.
type Locker interface { type Locker interface {
// TryLock will attempt to acquire the lock for key. If a // Lock acquires the lock for key, blocking until the lock
// lock could be obtained, nil values are returned as no // can be obtained or an error is returned. Note that, even
// waiting is required. If not (meaning another process is // after acquiring a lock, an idempotent operation may have
// already working on key), a Waiter value will be returned, // already been performed by another process that acquired
// upon which you should Wait() until it is finished. // the lock before - so always check to make sure idempotent
// operations still need to be performed after acquiring the
// lock.
// //
// The actual implementation of obtaining of a lock must be // The actual implementation of obtaining of a lock must be
// an atomic operation so that multiple TryLock calls at the // an atomic operation so that multiple Lock calls at the
// same time always results in only one caller receiving the // same time always results in only one caller receiving the
// lock. TryLock always returns without waiting. // lock at any given time.
// //
// To prevent deadlocks, all implementations (where this concern // To prevent deadlocks, all implementations (where this concern
// is relevant) should put a reasonable expiration on the lock in // is relevant) should put a reasonable expiration on the lock in
// case Unlock is unable to be called due to some sort of network // case Unlock is unable to be called due to some sort of network
// or system failure or crash. // or system failure or crash.
TryLock(key string) (Waiter, error) Lock(key string) error
// Unlock releases the lock for key. This method must ONLY be // Unlock releases the lock for key. This method must ONLY be
// called after a successful call to TryLock where no Waiter was // called after a successful call to TryLock where no Waiter was
...@@ -89,20 +91,6 @@ type Locker interface { ...@@ -89,20 +91,6 @@ 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.
type Waiter interface {
Wait()
} }
// KeyInfo holds information about a key in storage. // KeyInfo holds information about a key in storage.
...@@ -281,7 +269,7 @@ type ErrNotExist interface { ...@@ -281,7 +269,7 @@ type ErrNotExist interface {
// defaultFileStorage is a convenient, default storage // defaultFileStorage is a convenient, default storage
// implementation using the local file system. // implementation using the local file system.
var defaultFileStorage = FileStorage{Path: dataDir()} var defaultFileStorage = &FileStorage{Path: dataDir()}
// DefaultStorage is the default Storage implementation. // DefaultStorage is the default Storage implementation.
var DefaultStorage Storage = defaultFileStorage var DefaultStorage Storage = defaultFileStorage
...@@ -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": "fe722057f2654b33cd528b8fd8b90e53fa495564", "revision": "a3b276a1b44e1c2c3dcab752729976ea04f4839b",
"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