mirror of
https://github.com/caddyserver/caddy.git
synced 2025-03-10 23:59:01 +01:00
caddytls: Use latest certmagic package, with updated Storage interface
This commit is contained in:
parent
0684cf8611
commit
0b83014ff8
10 changed files with 284 additions and 229 deletions
1
caddy.go
1
caddy.go
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
func constructDefaultClusterPlugin() (certmagic.Storage, error) {
|
func constructDefaultClusterPlugin() (certmagic.Storage, error) {
|
||||||
return certmagic.FileStorage{Path: caddy.AssetsPath()}, nil
|
return &certmagic.FileStorage{Path: caddy.AssetsPath()}, nil
|
||||||
}
|
}
|
||||||
|
|
3
vendor/github.com/mholt/certmagic/certificates.go
generated
vendored
3
vendor/github.com/mholt/certmagic/certificates.go
generated
vendored
|
@ -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
|
||||||
|
|
35
vendor/github.com/mholt/certmagic/certmagic.go
generated
vendored
35
vendor/github.com/mholt/certmagic/certmagic.go
generated
vendored
|
@ -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 {
|
||||||
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) {
|
||||||
|
|
44
vendor/github.com/mholt/certmagic/client.go
generated
vendored
44
vendor/github.com/mholt/certmagic/client.go
generated
vendored
|
@ -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 {
|
||||||
// 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 {
|
||||||
// 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 {
|
||||||
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
|
||||||
|
|
66
vendor/github.com/mholt/certmagic/config.go
generated
vendored
66
vendor/github.com/mholt/certmagic/config.go
generated
vendored
|
@ -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 {
|
||||||
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 {
|
||||||
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,
|
||||||
|
|
||||||
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()
|
||||||
|
}
|
||||||
|
|
33
vendor/github.com/mholt/certmagic/crypto.go
generated
vendored
33
vendor/github.com/mholt/certmagic/crypto.go
generated
vendored
|
@ -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 {
|
||||||
}
|
}
|
||||||
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
293
vendor/github.com/mholt/certmagic/filestorage.go
generated
vendored
293
vendor/github.com/mholt/certmagic/filestorage.go
generated
vendored
|
@ -22,7 +22,6 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,10 +116,127 @@ 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
for {
|
||||||
|
err := createLockfile(filename)
|
||||||
|
if err == nil {
|
||||||
|
// got the lock, yay
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !os.IsExist(err) {
|
||||||
|
// unexpected error
|
||||||
|
return fmt.Errorf("creating lock file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock file already exists
|
||||||
|
|
||||||
|
info, err := os.Stat(filename)
|
||||||
|
switch {
|
||||||
|
case os.IsNotExist(err):
|
||||||
|
// must have just been removed; try again to create it
|
||||||
|
continue
|
||||||
|
|
||||||
|
case err != nil:
|
||||||
|
// unexpected error
|
||||||
|
return fmt.Errorf("accessing lock file: %v", err)
|
||||||
|
|
||||||
|
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, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock releases the lock for name.
|
||||||
|
func (fs *FileStorage) Unlock(key string) error {
|
||||||
|
return removeLockfile(fs.lockFilename(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FileStorage) String() string {
|
||||||
|
return "FileStorage:" + fs.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FileStorage) lockFilename(key string) string {
|
||||||
|
return filepath.Join(fs.lockDir(), StorageKeys.safe(key)+".lock")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FileStorage) lockDir() string {
|
||||||
|
return filepath.Join(fs.Path, "locks")
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileLockIsStale(info os.FileInfo) bool {
|
||||||
|
if info == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return time.Since(info.ModTime()) > staleLockDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
// homeDir returns the best guess of the current user's home
|
// homeDir returns the best guess of the current user's home
|
||||||
// directory from environment variables. If unknown, "." (the
|
// directory from environment variables. If unknown, "." (the
|
||||||
// current directory) is returned instead.
|
// current directory) is returned instead.
|
||||||
|
@ -149,160 +264,12 @@ func dataDir() string {
|
||||||
return filepath.Join(baseDir, "certmagic")
|
return filepath.Join(baseDir, "certmagic")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
fw = &fileStorageWaiter{
|
|
||||||
key: key,
|
|
||||||
filename: filepath.Join(lockDir, StorageKeys.safe(key)+".lock"),
|
|
||||||
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
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean up in memory
|
|
||||||
fw.wg.Done()
|
|
||||||
delete(fileStorageNameLocks, key)
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return filepath.Join(fs.Path, "locks")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs FileStorage) String() string {
|
|
||||||
return "FileStorage:" + fs.Path
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileStorageNameLocks = make(map[string]*fileStorageWaiter)
|
|
||||||
var fileStorageNameLocksMu sync.Mutex
|
|
||||||
|
|
||||||
var _ Storage = FileStorage{}
|
|
||||||
var _ Waiter = &fileStorageWaiter{}
|
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
34
vendor/github.com/mholt/certmagic/storage.go
generated
vendored
34
vendor/github.com/mholt/certmagic/storage.go
generated
vendored
|
@ -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 {
|
||||||
// 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 {
|
||||||
|
|
||||||
// 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
|
||||||
|
|
2
vendor/manifest
vendored
2
vendor/manifest
vendored
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue