mirror of
https://github.com/datarhei/core.git
synced 2025-10-05 07:57:13 +08:00
295 lines
8.1 KiB
Go
295 lines
8.1 KiB
Go
package autocert
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/datarhei/core/v16/log"
|
|
"github.com/datarhei/core/v16/slices"
|
|
|
|
"github.com/caddyserver/certmagic"
|
|
"github.com/klauspost/cpuid/v2"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type Manager interface {
|
|
CacheManagedCertificate(ctx context.Context, hostnames []string)
|
|
AcquireCertificates(ctx context.Context, hostnames []string) error
|
|
ManageCertificates(ctx context.Context, hostnames []string) error
|
|
HTTPChallengeResolver(ctx context.Context, listenAddress string) error
|
|
HTTPChallengeHandler(h http.Handler) http.Handler
|
|
GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error)
|
|
TLSConfig() *tls.Config
|
|
ManagedNames() []string
|
|
}
|
|
|
|
type Config struct {
|
|
Storage certmagic.Storage
|
|
DefaultHostname string
|
|
EmailAddress string
|
|
IsProduction bool
|
|
Logger log.Logger
|
|
}
|
|
|
|
type manager struct {
|
|
config *certmagic.Config
|
|
|
|
hostnames []string
|
|
lock sync.Mutex
|
|
|
|
logger log.Logger
|
|
}
|
|
|
|
func New(config Config) (Manager, error) {
|
|
m := &manager{
|
|
hostnames: []string{},
|
|
logger: config.Logger,
|
|
}
|
|
|
|
if m.logger == nil {
|
|
m.logger = log.New("")
|
|
}
|
|
|
|
certmagic.Default.Storage = config.Storage
|
|
certmagic.Default.DefaultServerName = config.DefaultHostname
|
|
certmagic.Default.Logger = zap.NewNop()
|
|
|
|
ca := certmagic.LetsEncryptStagingCA
|
|
if config.IsProduction {
|
|
ca = certmagic.LetsEncryptProductionCA
|
|
m.logger.Info().WithField("ca", ca).Log("Using production CA")
|
|
} else {
|
|
m.logger.Info().WithField("ca", ca).Log("Using staging CA")
|
|
}
|
|
|
|
certmagic.DefaultACME.Agreed = true
|
|
certmagic.DefaultACME.Email = config.EmailAddress
|
|
certmagic.DefaultACME.CA = ca
|
|
certmagic.DefaultACME.DisableHTTPChallenge = false
|
|
certmagic.DefaultACME.DisableTLSALPNChallenge = true
|
|
certmagic.DefaultACME.Logger = zap.NewNop()
|
|
|
|
magic := certmagic.NewDefault()
|
|
acme := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME)
|
|
acme.Logger = zap.NewNop()
|
|
|
|
magic.Issuers = []certmagic.Issuer{acme}
|
|
magic.Logger = zap.NewNop()
|
|
|
|
m.config = magic
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// HTTPChallengeHandler wraps h in a handler that can solve the ACME
|
|
// HTTP challenge. cfg is required, and it must have a certificate
|
|
// cache backed by a functional storage facility, since that is where
|
|
// the challenge state is stored between initiation and solution.
|
|
//
|
|
// If a request is not an ACME HTTP challenge, h will be invoked.
|
|
func (m *manager) HTTPChallengeHandler(h http.Handler) http.Handler {
|
|
acme := m.config.Issuers[0].(*certmagic.ACMEIssuer)
|
|
return acme.HTTPChallengeHandler(h)
|
|
}
|
|
|
|
// GetCertificate gets a certificate to satisfy clientHello. In getting
|
|
// the certificate, it abides the rules and settings defined in the Config
|
|
// that matches clientHello.ServerName. It tries to get certificates in
|
|
// this order:
|
|
//
|
|
// 1. Exact match in the in-memory cache
|
|
// 2. Wildcard match in the in-memory cache
|
|
// 3. Managers (if any)
|
|
// 4. Storage (if on-demand is enabled)
|
|
// 5. Issuers (if on-demand is enabled)
|
|
//
|
|
// This method is safe for use as a tls.Config.GetCertificate callback.
|
|
//
|
|
// GetCertificate will run in a new context.
|
|
func (m *manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
return m.config.GetCertificate(hello)
|
|
}
|
|
|
|
// HTTPChallengeResolver starts a http server that responds to HTTP challenge requests and returns
|
|
// as soon as the server is running. Use the context to stop the server.
|
|
func (m *manager) HTTPChallengeResolver(ctx context.Context, listenAddress string) error {
|
|
acme := m.config.Issuers[0].(*certmagic.ACMEIssuer)
|
|
|
|
// Start temporary http server on configured port
|
|
tempserver := &http.Server{
|
|
Addr: listenAddress,
|
|
Handler: acme.HTTPChallengeHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
})),
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
MaxHeaderBytes: 1 << 20,
|
|
}
|
|
|
|
errorCh := make(chan error, 1)
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
|
|
go func(errorCh chan<- error) {
|
|
wg.Done()
|
|
errorCh <- tempserver.ListenAndServe()
|
|
}(errorCh)
|
|
|
|
wg.Wait()
|
|
|
|
// Wait for an error
|
|
select {
|
|
case err := <-errorCh:
|
|
return err
|
|
case <-time.After(3 * time.Second):
|
|
break
|
|
}
|
|
|
|
go func(ctx context.Context) {
|
|
<-ctx.Done()
|
|
tempserver.Close()
|
|
|
|
// Drain and close the channel
|
|
select {
|
|
case <-errorCh:
|
|
default:
|
|
}
|
|
|
|
close(errorCh)
|
|
}(ctx)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *manager) CacheManagedCertificate(ctx context.Context, hostnames []string) {
|
|
for _, name := range hostnames {
|
|
m.config.CacheManagedCertificate(ctx, name)
|
|
}
|
|
}
|
|
|
|
// AcquireCertificates tries to acquire the certificates for the given hostnames synchronously.
|
|
func (m *manager) AcquireCertificates(ctx context.Context, hostnames []string) error {
|
|
m.lock.Lock()
|
|
added, removed := slices.DiffComparable(hostnames, m.hostnames)
|
|
m.lock.Unlock()
|
|
|
|
var err error
|
|
|
|
if len(added) != 0 {
|
|
// Get the certificates
|
|
m.logger.WithField("hostnames", added).Info().Log("Acquiring certificates ...")
|
|
|
|
err = m.config.ManageSync(ctx, added)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to acquire certificate for %s: %w", strings.Join(added, ","), err)
|
|
}
|
|
|
|
m.logger.WithField("hostnames", added).Info().Log("Successfully acquired certificate")
|
|
}
|
|
|
|
if len(removed) != 0 {
|
|
m.logger.WithField("hostnames", removed).Info().Log("Unmanage certificates")
|
|
}
|
|
|
|
m.lock.Lock()
|
|
m.hostnames = make([]string, len(hostnames))
|
|
copy(m.hostnames, hostnames)
|
|
m.lock.Unlock()
|
|
|
|
return err
|
|
}
|
|
|
|
// ManageCertificates is the same as AcquireCertificates but it does it in the background.
|
|
func (m *manager) ManageCertificates(ctx context.Context, hostnames []string) error {
|
|
m.lock.Lock()
|
|
added, removed := slices.DiffComparable(hostnames, m.hostnames)
|
|
m.hostnames = make([]string, len(hostnames))
|
|
copy(m.hostnames, hostnames)
|
|
m.lock.Unlock()
|
|
|
|
if len(removed) != 0 {
|
|
m.logger.WithField("hostnames", removed).Info().Log("Unmanage certificates")
|
|
}
|
|
|
|
if len(added) == 0 {
|
|
return nil
|
|
}
|
|
|
|
m.logger.WithField("hostnames", added).Info().Log("Acquiring certificates")
|
|
|
|
return m.config.ManageAsync(ctx, added)
|
|
}
|
|
|
|
// ManagedNames returns a list of the currently managed domain names.
|
|
func (m *manager) ManagedNames() []string {
|
|
m.lock.Lock()
|
|
defer m.lock.Unlock()
|
|
|
|
hostnames := make([]string, len(m.hostnames))
|
|
copy(hostnames, m.hostnames)
|
|
|
|
return hostnames
|
|
}
|
|
|
|
// TLSConfig is an opinionated method that returns a recommended, modern
|
|
// TLS configuration that can be used to configure TLS listeners. Aside
|
|
// from safe, modern defaults, this method sets one critical field on the
|
|
// TLS config which is required to enable automatic certificate
|
|
// management: GetCertificate.
|
|
//
|
|
// The GetCertificate field is necessary to get certificates from memory
|
|
// or storage, including both manual and automated certificates. You
|
|
// should only change this field if you know what you are doing.
|
|
func (m *manager) TLSConfig() *tls.Config {
|
|
return &tls.Config{
|
|
GetCertificate: m.GetCertificate,
|
|
|
|
// the rest recommended for modern TLS servers
|
|
MinVersion: tls.VersionTLS12,
|
|
CurvePreferences: []tls.CurveID{
|
|
tls.X25519,
|
|
tls.CurveP256,
|
|
},
|
|
CipherSuites: preferredDefaultCipherSuites(),
|
|
PreferServerCipherSuites: true,
|
|
}
|
|
}
|
|
|
|
// preferredDefaultCipherSuites returns an appropriate
|
|
// cipher suite to use depending on hardware support
|
|
// for AES-NI.
|
|
//
|
|
// See https://github.com/mholt/caddy/issues/1674
|
|
// Copied from https://github.com/caddyserver/certmagic/blob/d8e706f9b5011ecbaf20d3c1641e5446ad453613/crypto.go#L299
|
|
func preferredDefaultCipherSuites() []uint16 {
|
|
if cpuid.CPU.Supports(cpuid.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,
|
|
}
|
|
)
|