mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
ac4fa2c3a9
These changes span work from the last ~4 months in an effort to make Caddy more extensible, reduce the coupling between its components, and lay a more robust foundation of code going forward into 1.0. A bunch of new features have been added, too, with even higher future potential. The most significant design change is an overall inversion of dependencies. Instead of the caddy package knowing about the server and the notion of middleware and config, the caddy package exposes an interface that other components plug into. This does introduce more indirection when reading the code, but every piece is very modular and pluggable. Even the HTTP server is pluggable. The caddy package has been moved to the top level, and main has been pushed into a subfolder called caddy. The actual logic of the main file has been pushed even further into caddy/caddymain/run.go so that custom builds of Caddy can be 'go get'able. The HTTPS logic was surgically separated into two parts to divide the TLS-specific code and the HTTPS-specific code. The caddytls package can now be used by any type of server that needs TLS, not just HTTP. I also added the ability to customize nearly every aspect of TLS at the site level rather than all sites sharing the same TLS configuration. Not all of this flexibility is exposed in the Caddyfile yet, but it may be in the future. Caddy can also generate self-signed certificates in memory for the convenience of a developer working on localhost who wants HTTPS. And Caddy now supports the DNS challenge, assuming at least one DNS provider is plugged in. Dozens, if not hundreds, of other minor changes swept through the code base as I literally started from an empty main function, copying over functions or files as needed, then adjusting them to fit in the new design. Most tests have been restored and adapted to the new API, but more work is needed there. A lot of what was "impossible" before is now possible, or can be made possible with minimal disruption of the code. For example, it's fairly easy to make plugins hook into another part of the code via callbacks. Plugins can do more than just be directives; we now have plugins that customize how the Caddyfile is loaded (useful when you need to get your configuration from a remote store). Site addresses no longer need be just a host and port. They can have a path, allowing you to scope a configuration to a specific path. There is no inheretance, however; each site configuration is distinct. Thanks to amazing work by Lucas Clemente, this commit adds experimental QUIC support. Turn it on using the -quic flag; your browser may have to be configured to enable it. Almost everything is here, but you will notice that most of the middle- ware are missing. After those are transferred over, we'll be ready for beta tests. I'm very excited to get this out. Thanks for everyone's help and patience these last few months. I hope you like it!!
312 lines
11 KiB
Go
312 lines
11 KiB
Go
package caddytls
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// configGroup is a type that keys configs by their hostname
|
|
// (hostnames can have wildcard characters; use the getConfig
|
|
// method to get a config by matching its hostname). Its
|
|
// GetCertificate function can be used with tls.Config.
|
|
type configGroup map[string]*Config
|
|
|
|
// getConfig gets the config by the first key match for name.
|
|
// In other words, "sub.foo.bar" will get the config for "*.foo.bar"
|
|
// if that is the closest match. This function MAY return nil
|
|
// if no match is found.
|
|
//
|
|
// This function follows nearly the same logic to lookup
|
|
// a hostname as the getCertificate function uses.
|
|
func (cg configGroup) getConfig(name string) *Config {
|
|
name = strings.ToLower(name)
|
|
|
|
// exact match? great, let's use it
|
|
if config, ok := cg[name]; ok {
|
|
return config
|
|
}
|
|
|
|
// try replacing labels in the name with wildcards until we get a match
|
|
labels := strings.Split(name, ".")
|
|
for i := range labels {
|
|
labels[i] = "*"
|
|
candidate := strings.Join(labels, ".")
|
|
if config, ok := cg[candidate]; ok {
|
|
return config
|
|
}
|
|
}
|
|
|
|
// as last resort, try a config that serves all names
|
|
if config, ok := cg[""]; ok {
|
|
return config
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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 first checks the in-
|
|
// memory cache, then, if the config enables "OnDemand", it accessses
|
|
// disk, then accesses the network if it must obtain a new certificate
|
|
// via ACME.
|
|
//
|
|
// This method is safe for use as a tls.Config.GetCertificate callback.
|
|
func (cg configGroup) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
cert, err := cg.getCertDuringHandshake(clientHello.ServerName, true, true)
|
|
return &cert.Certificate, err
|
|
}
|
|
|
|
// getCertDuringHandshake will get a certificate for name. It first tries
|
|
// the in-memory cache. If no certificate for name is in the cache, the
|
|
// config most closely corresponding to name will be loaded. If that config
|
|
// allows it (OnDemand==true) and if loadIfNecessary == true, it goes to disk
|
|
// to load it into the cache and serve it. If it's not on disk and if
|
|
// obtainIfNecessary == true, the certificate will be obtained from the CA,
|
|
// cached, and served. If obtainIfNecessary is true, then loadIfNecessary
|
|
// must also be set to true. An error will be returned if and only if no
|
|
// certificate is available.
|
|
//
|
|
// This function is safe for concurrent use.
|
|
func (cg configGroup) getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) {
|
|
// First check our in-memory cache to see if we've already loaded it
|
|
cert, matched, defaulted := getCertificate(name)
|
|
if matched {
|
|
return cert, nil
|
|
}
|
|
|
|
// Get the relevant TLS config for this name. If OnDemand is enabled,
|
|
// then we might be able to load or obtain a needed certificate.
|
|
cfg := cg.getConfig(name)
|
|
if cfg != nil && cfg.OnDemand && loadIfNecessary {
|
|
// Then check to see if we have one on disk
|
|
loadedCert, err := CacheManagedCertificate(name, cfg)
|
|
if err == nil {
|
|
loadedCert, err = cg.handshakeMaintenance(name, loadedCert)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err)
|
|
}
|
|
return loadedCert, nil
|
|
}
|
|
if obtainIfNecessary {
|
|
// By this point, we need to ask the CA for a certificate
|
|
|
|
name = strings.ToLower(name)
|
|
|
|
// Make sure aren't over any applicable limits
|
|
err := cg.checkLimitsForObtainingNewCerts(name)
|
|
if err != nil {
|
|
return Certificate{}, err
|
|
}
|
|
|
|
// Name has to qualify for a certificate
|
|
if !HostQualifies(name) {
|
|
return cert, errors.New("hostname '" + name + "' does not qualify for certificate")
|
|
}
|
|
|
|
// Obtain certificate from the CA
|
|
return cg.obtainOnDemandCertificate(name, cfg)
|
|
}
|
|
}
|
|
|
|
// Fall back to the default certificate if there is one
|
|
if defaulted {
|
|
return cert, nil
|
|
}
|
|
|
|
return Certificate{}, fmt.Errorf("no certificate available for %s", name)
|
|
}
|
|
|
|
// checkLimitsForObtainingNewCerts checks to see if name can be issued right
|
|
// now according to mitigating factors we keep track of and preferences the
|
|
// user has set. If a non-nil error is returned, do not issue a new certificate
|
|
// for name.
|
|
func (cg configGroup) checkLimitsForObtainingNewCerts(name string) error {
|
|
// User can set hard limit for number of certs for the process to issue
|
|
if onDemandMaxIssue > 0 && atomic.LoadInt32(OnDemandIssuedCount) >= onDemandMaxIssue {
|
|
return fmt.Errorf("%s: maximum certificates issued (%d)", name, onDemandMaxIssue)
|
|
}
|
|
|
|
// Make sure name hasn't failed a challenge recently
|
|
failedIssuanceMu.RLock()
|
|
when, ok := failedIssuance[name]
|
|
failedIssuanceMu.RUnlock()
|
|
if ok {
|
|
return fmt.Errorf("%s: throttled; refusing to issue cert since last attempt on %s failed", name, when.String())
|
|
}
|
|
|
|
// Make sure, if we've issued a few certificates already, that we haven't
|
|
// issued any recently
|
|
lastIssueTimeMu.Lock()
|
|
since := time.Since(lastIssueTime)
|
|
lastIssueTimeMu.Unlock()
|
|
if atomic.LoadInt32(OnDemandIssuedCount) >= 10 && since < 10*time.Minute {
|
|
return fmt.Errorf("%s: throttled; last certificate was obtained %v ago", name, since)
|
|
}
|
|
|
|
// 👍Good to go
|
|
return nil
|
|
}
|
|
|
|
// obtainOnDemandCertificate obtains a certificate for name for the given
|
|
// name. If another goroutine has already started obtaining a cert for
|
|
// name, it will wait and use what the other goroutine obtained.
|
|
//
|
|
// This function is safe for use by multiple concurrent goroutines.
|
|
func (cg configGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certificate, error) {
|
|
// We must protect this process from happening concurrently, so synchronize.
|
|
obtainCertWaitChansMu.Lock()
|
|
wait, ok := obtainCertWaitChans[name]
|
|
if ok {
|
|
// lucky us -- another goroutine is already obtaining the certificate.
|
|
// wait for it to finish obtaining the cert and then we'll use it.
|
|
obtainCertWaitChansMu.Unlock()
|
|
<-wait
|
|
return cg.getCertDuringHandshake(name, true, false)
|
|
}
|
|
|
|
// looks like it's up to us to do all the work and obtain the cert
|
|
wait = make(chan struct{})
|
|
obtainCertWaitChans[name] = wait
|
|
obtainCertWaitChansMu.Unlock()
|
|
|
|
// Unblock waiters and delete waitgroup when we return
|
|
defer func() {
|
|
obtainCertWaitChansMu.Lock()
|
|
close(wait)
|
|
delete(obtainCertWaitChans, name)
|
|
obtainCertWaitChansMu.Unlock()
|
|
}()
|
|
|
|
log.Printf("[INFO] Obtaining new certificate for %s", name)
|
|
|
|
if err := cfg.obtainCertName(name, false); err != nil {
|
|
// Failed to solve challenge, so don't allow another on-demand
|
|
// issue for this name to be attempted for a little while.
|
|
failedIssuanceMu.Lock()
|
|
failedIssuance[name] = time.Now()
|
|
go func(name string) {
|
|
time.Sleep(5 * time.Minute)
|
|
failedIssuanceMu.Lock()
|
|
delete(failedIssuance, name)
|
|
failedIssuanceMu.Unlock()
|
|
}(name)
|
|
failedIssuanceMu.Unlock()
|
|
return Certificate{}, err
|
|
}
|
|
|
|
// Success - update counters and stuff
|
|
atomic.AddInt32(OnDemandIssuedCount, 1)
|
|
lastIssueTimeMu.Lock()
|
|
lastIssueTime = time.Now()
|
|
lastIssueTimeMu.Unlock()
|
|
|
|
// The certificate is already on disk; now just start over to load it and serve it
|
|
return cg.getCertDuringHandshake(name, true, false)
|
|
}
|
|
|
|
// handshakeMaintenance performs a check on cert for expiration and OCSP
|
|
// validity.
|
|
//
|
|
// This function is safe for use by multiple concurrent goroutines.
|
|
func (cg configGroup) handshakeMaintenance(name string, cert Certificate) (Certificate, error) {
|
|
// Check cert expiration
|
|
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
|
|
if timeLeft < RenewDurationBefore {
|
|
log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft)
|
|
return cg.renewDynamicCertificate(name, cert.Config)
|
|
}
|
|
|
|
// Check OCSP staple validity
|
|
if cert.OCSP != nil {
|
|
refreshTime := cert.OCSP.ThisUpdate.Add(cert.OCSP.NextUpdate.Sub(cert.OCSP.ThisUpdate) / 2)
|
|
if time.Now().After(refreshTime) {
|
|
err := stapleOCSP(&cert, nil)
|
|
if err != nil {
|
|
// An error with OCSP stapling is not the end of the world, and in fact, is
|
|
// quite common considering not all certs have issuer URLs that support it.
|
|
log.Printf("[ERROR] Getting OCSP for %s: %v", name, err)
|
|
}
|
|
certCacheMu.Lock()
|
|
certCache[name] = cert
|
|
certCacheMu.Unlock()
|
|
}
|
|
}
|
|
|
|
return cert, nil
|
|
}
|
|
|
|
// renewDynamicCertificate renews the certificate for name using cfg. It returns the
|
|
// certificate to use and an error, if any. currentCert may be returned even if an
|
|
// error occurs, since we perform renewals before they expire and it may still be
|
|
// usable. name should already be lower-cased before calling this function.
|
|
//
|
|
// This function is safe for use by multiple concurrent goroutines.
|
|
func (cg configGroup) renewDynamicCertificate(name string, cfg *Config) (Certificate, error) {
|
|
obtainCertWaitChansMu.Lock()
|
|
wait, ok := obtainCertWaitChans[name]
|
|
if ok {
|
|
// lucky us -- another goroutine is already renewing the certificate.
|
|
// wait for it to finish, then we'll use the new one.
|
|
obtainCertWaitChansMu.Unlock()
|
|
<-wait
|
|
return cg.getCertDuringHandshake(name, true, false)
|
|
}
|
|
|
|
// looks like it's up to us to do all the work and renew the cert
|
|
wait = make(chan struct{})
|
|
obtainCertWaitChans[name] = wait
|
|
obtainCertWaitChansMu.Unlock()
|
|
|
|
// unblock waiters and delete waitgroup when we return
|
|
defer func() {
|
|
obtainCertWaitChansMu.Lock()
|
|
close(wait)
|
|
delete(obtainCertWaitChans, name)
|
|
obtainCertWaitChansMu.Unlock()
|
|
}()
|
|
|
|
log.Printf("[INFO] Renewing certificate for %s", name)
|
|
|
|
err := cfg.renewCertName(name, false)
|
|
if err != nil {
|
|
return Certificate{}, err
|
|
}
|
|
|
|
return cg.getCertDuringHandshake(name, true, false)
|
|
}
|
|
|
|
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
|
|
var obtainCertWaitChans = make(map[string]chan struct{})
|
|
var obtainCertWaitChansMu sync.Mutex
|
|
|
|
// OnDemandIssuedCount is the number of certificates that have been issued
|
|
// on-demand by this process. It is only safe to modify this count atomically.
|
|
// If it reaches onDemandMaxIssue, on-demand issuances will fail.
|
|
var OnDemandIssuedCount = new(int32)
|
|
|
|
// onDemandMaxIssue is set based on max_certs in tls config. It specifies the
|
|
// maximum number of certificates that can be issued.
|
|
// TODO: This applies globally, but we should probably make a server-specific
|
|
// way to keep track of these limits and counts, since it's specified in the
|
|
// Caddyfile...
|
|
var onDemandMaxIssue int32
|
|
|
|
// failedIssuance is a set of names that we recently failed to get a
|
|
// certificate for from the ACME CA. They are removed after some time.
|
|
// When a name is in this map, do not issue a certificate for it on-demand.
|
|
var failedIssuance = make(map[string]time.Time)
|
|
var failedIssuanceMu sync.RWMutex
|
|
|
|
// lastIssueTime records when we last obtained a certificate successfully.
|
|
// If this value is recent, do not make any on-demand certificate requests.
|
|
var lastIssueTime time.Time
|
|
var lastIssueTimeMu sync.Mutex
|
|
|
|
var errNoCert = errors.New("no certificate available")
|