mirror of
https://github.com/jhillyerd/inbucket.git
synced 2026-10-29 05:32:31 +00:00
117 lines
3.7 KiB
Go
117 lines
3.7 KiB
Go
// Package tlscert provides X509 key pairs that follow changes to their files on disk.
|
|
package tlscert
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// retryInterval is the minimum time between attempts to reload a key pair that failed to load
|
|
// while its files are unchanged, such as when a chmod or chown fixes its permissions.
|
|
const retryInterval = 30 * time.Second
|
|
|
|
// fileState identifies a version of a file by its modification time and size.
|
|
type fileState struct {
|
|
modTime time.Time
|
|
size int64
|
|
}
|
|
|
|
// Reloader serves a certificate and private key loaded from files, reloading them when either
|
|
// file changes. It is safe for concurrent use.
|
|
type Reloader struct {
|
|
certFile string // X509 public certificate file.
|
|
keyFile string // X509 private key file.
|
|
logger zerolog.Logger // Logger for reload events.
|
|
retryInterval time.Duration // Minimum time between retries of a failed, unchanged pair.
|
|
now func() time.Time // Clock used to schedule retries.
|
|
|
|
mu sync.Mutex // Guards the fields below.
|
|
cert *tls.Certificate // Last successfully loaded key pair.
|
|
certState fileState // State of certFile at the last load attempt.
|
|
keyState fileState // State of keyFile at the last load attempt.
|
|
failedAt time.Time // Time of the last failed load attempt, zero after a success.
|
|
}
|
|
|
|
// NewReloader loads the key pair from certFile and keyFile, returning an error if it cannot be
|
|
// loaded.
|
|
func NewReloader(certFile, keyFile string, logger zerolog.Logger) (*Reloader, error) {
|
|
r := &Reloader{
|
|
certFile: certFile,
|
|
keyFile: keyFile,
|
|
logger: logger,
|
|
retryInterval: retryInterval,
|
|
now: time.Now,
|
|
}
|
|
certState, keyState, err := r.stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.cert = &cert
|
|
r.certState = certState
|
|
r.keyState = keyState
|
|
return r, nil
|
|
}
|
|
|
|
// GetCertificate returns the current key pair, reloading it first if either file changed since
|
|
// the last load attempt, or if the last attempt failed and retryInterval has elapsed. Failures
|
|
// are logged and the previous key pair is served. crypto/tls calls it on full handshakes only.
|
|
func (r *Reloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
// On error, stat returns zero states, so repeated stat failures count as unchanged files.
|
|
certState, keyState, err := r.stat()
|
|
changed := certState != r.certState || keyState != r.keyState
|
|
retryDue := !r.failedAt.IsZero() && r.now().Sub(r.failedAt) >= r.retryInterval
|
|
if !changed && !retryDue {
|
|
return r.cert, nil
|
|
}
|
|
|
|
r.certState = certState
|
|
r.keyState = keyState
|
|
if err == nil {
|
|
var cert tls.Certificate
|
|
cert, err = tls.LoadX509KeyPair(r.certFile, r.keyFile)
|
|
if err == nil {
|
|
r.cert = &cert
|
|
r.failedAt = time.Time{}
|
|
r.logger.Info().Msg("Reloaded X509 KeyPair")
|
|
return r.cert, nil
|
|
}
|
|
}
|
|
r.failedAt = r.now()
|
|
r.logger.Warn().Err(err).Msg("Failed to reload X509 KeyPair, serving previous certificate")
|
|
return r.cert, nil
|
|
}
|
|
|
|
// stat returns the current state of the certificate and key files, or zero states and an error
|
|
// if either cannot be read.
|
|
func (r *Reloader) stat() (certState, keyState fileState, err error) {
|
|
certState, err = statFile(r.certFile)
|
|
if err != nil {
|
|
return fileState{}, fileState{}, err
|
|
}
|
|
keyState, err = statFile(r.keyFile)
|
|
if err != nil {
|
|
return fileState{}, fileState{}, err
|
|
}
|
|
return certState, keyState, nil
|
|
}
|
|
|
|
func statFile(name string) (fileState, error) {
|
|
info, err := os.Stat(name)
|
|
if err != nil {
|
|
return fileState{}, fmt.Errorf("stat %s: %w", name, err)
|
|
}
|
|
return fileState{modTime: info.ModTime(), size: info.Size()}, nil
|
|
}
|