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) {
return nil, fmt.Errorf("constructing cluster plugin %s: %v", clusterPluginName, err)
}
certmagic.DefaultStorage = storage
OnProcessExit = append(OnProcessExit, certmagic.DefaultStorage.UnlockAllObtained)
}
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 {
}
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 {
if c.NotAfter.IsZero() {
return false
}
timeLeft := c.NotAfter.UTC().Sub(time.Now().UTC())
renewDurationBefore := DefaultRenewDurationBefore
if len(c.configs) > 0 && c.configs[0].RenewDurationBefore > 0 {
renewDurationBefore = c.configs[0].RenewDurationBefore
}
return timeLeft < renewDurationBefore
return time.Until(c.NotAfter) < renewDurationBefore
}
// CacheManagedCertificate loads the certificate for domain into the
......
......@@ -52,6 +52,16 @@ import (
// HTTPS serves mux for all domainNames using the HTTP
// 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
// the CA's Subscriber Agreement and/or Terms of Service.
func HTTPS(domainNames []string, mux http.Handler) error {
......@@ -96,13 +106,32 @@ func HTTPS(domainNames []string, mux http.Handler) error {
hln, hsln := httpLn, httpsLn
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",
domainNames, hln.Addr(), hsln.Addr())
go http.Serve(hln, httpHandler)
return http.Serve(hsln, mux)
go httpServer.Serve(hln)
return httpsServer.Serve(hsln)
}
func httpRedirectHandler(w http.ResponseWriter, r *http.Request) {
......
......@@ -208,6 +208,8 @@ func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) {
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 {
return fmt.Sprintf("%s:%s:%s", op, domainName, cfg.CA)
}
......@@ -215,30 +217,34 @@ func (cfg *Config) lockKey(op, domainName string) string {
// Obtain obtains a single certificate for name. It stores the certificate
// on the disk if successful. This function is safe for concurrent use.
//
// Right now our storage mechanism only supports one name per certificate,
// so this function (along with Renew and Revoke) only accepts one domain
// as input. It can be easily modified to support SAN certificates if our
// storage mechanism is upgraded later.
// Our storage mechanism only supports one name per certificate, so this
// function (along with Renew and Revoke) only accepts one domain as input.
// It could be easily modified to support SAN certificates if our storage
// 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
// method on that instead of this lower-level method.
func (c *acmeClient) Obtain(name string) error {
// ensure idempotency of the obtain operation for this 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 {
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() {
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++ {
request := certificate.ObtainRequest{
Domains: []string{name},
......@@ -280,19 +286,15 @@ func (c *acmeClient) Obtain(name string) error {
// Callers who have access to a Config value should use the RenewCert
// method on that instead of this lower-level method.
func (c *acmeClient) Renew(name string) error {
// ensure idempotency of the renew operation for this 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 {
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() {
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 {
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.
var newCertMeta *certificate.Resource
var success bool
......
......@@ -21,6 +21,7 @@ import (
"time"
"github.com/xenolf/lego/certcrypto"
"github.com/xenolf/lego/certificate"
"github.com/xenolf/lego/challenge"
"github.com/xenolf/lego/challenge/tlsalpn01"
"github.com/xenolf/lego/lego"
......@@ -277,12 +278,6 @@ func (cfg *Config) ObtainCert(name string, interactive bool) error {
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)
if err != nil {
return err
......@@ -317,24 +312,37 @@ func (cfg *Config) RevokeCert(domain string, interactive bool) error {
return client.Revoke(domain)
}
// TLSConfig returns a TLS configuration that
// can be used to configure TLS listeners. It
// supports the TLS-ALPN challenge and serves
// up certificates managed by cfg.
// TLSConfig is an opinionated method that returns a
// recommended, modern TLS configuration that can be
// used to configure TLS listeners, which also supports
// 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 {
return &tls.Config{
// these two fields necessary for TLS-ALPN challenge
GetCertificate: cfg.GetCertificate,
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
// obtaining or renewing a certificate with ACME, and returns
// whether this name should be skipped (like if it's not
......@@ -356,3 +364,27 @@ func (cfg *Config) preObtainOrRenewChecks(name string, allowPrompts bool) (bool,
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 (
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"hash/fnv"
"github.com/klauspost/cpuid"
"github.com/xenolf/lego/certificate"
)
......@@ -153,3 +155,34 @@ func hashCertificateChain(certChain [][]byte) string {
}
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 (
"path"
"path/filepath"
"runtime"
"sync"
"time"
)
......@@ -34,13 +33,13 @@ type FileStorage struct {
}
// 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))
return !os.IsNotExist(err)
}
// 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)
err := os.MkdirAll(filepath.Dir(filename), 0700)
if err != nil {
......@@ -50,7 +49,7 @@ func (fs FileStorage) Store(key string, value []byte) error {
}
// 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))
if os.IsNotExist(err) {
return nil, ErrNotExist(err)
......@@ -59,8 +58,7 @@ func (fs FileStorage) Load(key string) ([]byte, error) {
}
// 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))
if os.IsNotExist(err) {
return ErrNotExist(err)
......@@ -69,7 +67,7 @@ func (fs FileStorage) Delete(key string) error {
}
// 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
walkPrefix := fs.Filename(prefix)
......@@ -100,7 +98,7 @@ func (fs FileStorage) List(prefix string, recursive bool) ([]string, error) {
}
// 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))
if os.IsNotExist(err) {
return KeyInfo{}, ErrNotExist(err)
......@@ -118,191 +116,160 @@ func (fs FileStorage) Stat(key string) (KeyInfo, error) {
// Filename returns the key as a path on the file
// 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))
}
// homeDir returns the best guess of the current user's home
// directory from environment variables. If unknown, "." (the
// current directory) is returned instead.
func homeDir() string {
home := os.Getenv("HOME")
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")
}
// Lock obtains a lock named by the given key. It blocks
// until the lock can be obtained or an error is returned.
func (fs *FileStorage) Lock(key string) error {
start := time.Now()
filename := fs.lockFilename(key)
// TryLock attempts to get a lock for name, otherwise it returns
// a Waiter value to wait until the other process is finished.
func (fs FileStorage) TryLock(key string) (Waiter, error) {
fileStorageNameLocksMu.Lock()
defer fileStorageNameLocksMu.Unlock()
// 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
for {
err := createLockfile(filename)
if err == nil {
// got the lock, yay
return nil
}
// attempt to persist lock to disk by creating lock file
// parent dir must exist
lockDir := fs.lockDir()
if err := os.MkdirAll(lockDir, 0700); err != nil {
return nil, err
if !os.IsExist(err) {
// unexpected error
return fmt.Errorf("creating lock file: %v", err)
}
fw = &fileStorageWaiter{
key: key,
filename: filepath.Join(lockDir, StorageKeys.safe(key)+".lock"),
wg: new(sync.WaitGroup),
}
// lock file already exists
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:
// create the file in a special mode such that an
// error is returned if it already exists
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
case err != nil:
// unexpected error
return fmt.Errorf("accessing lock file: %v", err)
// check to see if the lock is stale, if we haven't already
if !checkedStaleLock {
checkedStaleLock = true
if fs.lockFileStale(fw.filename) {
case fileLockIsStale(info):
// lock file is stale - delete it and try again to create one
log.Printf("[INFO][%s] Lock for '%s' is stale; removing then retrying: %s",
fs, key, fw.filename)
os.Remove(fw.filename)
goto createLock
fs, key, filename)
removeLockfile(filename)
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.
func (fs FileStorage) Unlock(key string) error {
fileStorageNameLocksMu.Lock()
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()
}
func (fs *FileStorage) Unlock(key string) error {
return removeLockfile(fs.lockFilename(key))
}
// clean up in memory
fw.wg.Done()
delete(fileStorageNameLocks, key)
func (fs *FileStorage) String() string {
return "FileStorage:" + fs.Path
}
return nil
func (fs *FileStorage) lockFilename(key string) string {
return filepath.Join(fs.lockDir(), StorageKeys.safe(key)+".lock")
}
// 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) lockDir() string {
return filepath.Join(fs.Path, "locks")
}
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?
func fileLockIsStale(info os.FileInfo) bool {
if info == nil {
return true
}
return time.Since(info.ModTime()) > staleLockDuration
}
func (fs FileStorage) lockDir() string {
return filepath.Join(fs.Path, "locks")
// createLockfile atomically creates the lockfile
// 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 {
return "FileStorage:" + fs.Path
// removeLockfile atomically removes filename,
// 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
// polls the file system to check for the existence of
// a file. It also uses a WaitGroup to optimize the
// polling in the case when this process is the only
// one waiting. (Other processes that are waiting for
// the lock will still block, but must wait for the
// polling to get their answer.)
type fileStorageWaiter struct {
key string
filename string
wg *sync.WaitGroup
// atomicallyCreateFile atomically creates the file
// identified by filename if it doesn't already exist.
func atomicallyCreateFile(filename string) error {
// no need to check this, we only really care about the file creation error
os.MkdirAll(filepath.Dir(filename), 0700)
f, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL, 0644)
if err == nil {
f.Close()
}
return err
}
// Wait waits until the lock is released.
func (fw *fileStorageWaiter) Wait() {
start := time.Now()
fw.wg.Wait()
for time.Since(start) < 1*time.Hour {
_, err := os.Stat(fw.filename)
if os.IsNotExist(err) {
return
// homeDir returns the best guess of the current user's home
// directory from environment variables. If unknown, "." (the
// current directory) is returned instead.
func homeDir() string {
home := os.Getenv("HOME")
if home == "" && runtime.GOOS == "windows" {
drive := os.Getenv("HOMEDRIVE")
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)
var fileStorageNameLocksMu sync.Mutex
var _ Storage = FileStorage{}
var _ Waiter = &fileStorageWaiter{}
func dataDir() string {
baseDir := filepath.Join(homeDir(), ".local", "share")
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
baseDir = xdgData
}
return filepath.Join(baseDir, "certmagic")
}
// staleLockDuration is the length of time
// before considering a lock to be stale.
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 {
// Locker facilitates synchronization of certificate tasks across
// machines and networks.
type Locker interface {
// TryLock will attempt to acquire the lock for key. If a
// lock could be obtained, nil values are returned as no
// waiting is required. If not (meaning another process is
// already working on key), a Waiter value will be returned,
// upon which you should Wait() until it is finished.
// Lock acquires the lock for key, blocking until the lock
// can be obtained or an error is returned. Note that, even
// after acquiring a lock, an idempotent operation may have
// already been performed by another process that acquired
// 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
// 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
// lock. TryLock always returns without waiting.
// lock at any given time.
//
// To prevent deadlocks, all implementations (where this concern
// is relevant) should put a reasonable expiration on the lock in
// case Unlock is unable to be called due to some sort of network
// 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
// called after a successful call to TryLock where no Waiter was
......@@ -89,20 +91,6 @@ type Locker interface {
// TryLock or if Unlock was not called at all. Unlock should also
// clean up any unused resources allocated during TryLock.
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.
......@@ -281,7 +269,7 @@ type ErrNotExist interface {
// defaultFileStorage is a convenient, default storage
// implementation using the local file system.
var defaultFileStorage = FileStorage{Path: dataDir()}
var defaultFileStorage = &FileStorage{Path: dataDir()}
// DefaultStorage is the default Storage implementation.
var DefaultStorage Storage = defaultFileStorage
......@@ -138,7 +138,7 @@
"importpath": "github.com/mholt/certmagic",
"repository": "https://github.com/mholt/certmagic",
"vcs": "git",
"revision": "fe722057f2654b33cd528b8fd8b90e53fa495564",
"revision": "a3b276a1b44e1c2c3dcab752729976ea04f4839b",
"branch": "master",
"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