Commit da8a4faf authored by Matthew Holt's avatar Matthew Holt

letsencrypt: Use existing certs & keys if already in storage

parent 9f9de389
// Package letsencrypt integrates Let's Encrypt with Caddy with first-class support.
// It is designed to configure sites for HTTPS by default.
package letsencrypt package letsencrypt
import ( import (
...@@ -26,6 +28,15 @@ import ( ...@@ -26,6 +28,15 @@ import (
// address from last time. If there isn't one, the user // address from last time. If there isn't one, the user
// will be prompted. If the user leaves email blank, <TODO>. // will be prompted. If the user leaves email blank, <TODO>.
func Activate(configs []server.Config) ([]server.Config, error) { func Activate(configs []server.Config) ([]server.Config, error) {
// First identify and configure any elligible hosts for which
// we already have certs and keys in storage from last time.
configLen := len(configs) // avoid infinite loop since this loop appends to the slice
for i := 0; i < configLen; i++ {
if existingCertAndKey(configs[i].Host) {
configs = autoConfigure(&configs[i], configs)
}
}
// Group configs by LE email address; this will help us // Group configs by LE email address; this will help us
// reduce round-trips when getting the certs. // reduce round-trips when getting the certs.
initMap, err := groupConfigsByEmail(configs) initMap, err := groupConfigsByEmail(configs)
...@@ -54,8 +65,10 @@ func Activate(configs []server.Config) ([]server.Config, error) { ...@@ -54,8 +65,10 @@ func Activate(configs []server.Config) ([]server.Config, error) {
return configs, err return configs, err
} }
// it all comes down to this: filling in the file path of a valid certificate automatically // it all comes down to this: turning TLS on for all the configs
configs = autoConfigure(configs, serverConfigs) for _, cfg := range serverConfigs {
configs = autoConfigure(cfg, configs)
}
} }
return configs, nil return configs, nil
...@@ -64,10 +77,18 @@ func Activate(configs []server.Config) ([]server.Config, error) { ...@@ -64,10 +77,18 @@ func Activate(configs []server.Config) ([]server.Config, error) {
// groupConfigsByEmail groups configs by the Let's Encrypt email address // groupConfigsByEmail groups configs by the Let's Encrypt email address
// associated to them or to the default Let's Encrypt email address. If the // associated to them or to the default Let's Encrypt email address. If the
// default email is not available, the user will be prompted to provide one. // default email is not available, the user will be prompted to provide one.
//
// This function also filters out configs that don't need extra TLS help.
// Configurations with a manual TLS configuration or one that is already
// found in storage will not be added to any group.
func groupConfigsByEmail(configs []server.Config) (map[string][]*server.Config, error) { func groupConfigsByEmail(configs []server.Config) (map[string][]*server.Config, error) {
initMap := make(map[string][]*server.Config) initMap := make(map[string][]*server.Config)
for i := 0; i < len(configs); i++ { for i := 0; i < len(configs); i++ {
if configs[i].TLS.Certificate == "" && configs[i].TLS.Key == "" && configs[i].Port != "http" { // TODO: && !cfg.Host.IsLoopback() if configs[i].TLS.Certificate == "" && configs[i].TLS.Key == "" && configs[i].Port != "http" { // TODO: && !cfg.Host.IsLoopback()
// make sure an HTTPS version of this config doesn't exist in the list already
if hostHasOtherScheme(configs[i].Host, "https", configs) {
continue
}
leEmail := getEmail(configs[i]) leEmail := getEmail(configs[i])
if leEmail == "" { if leEmail == "" {
return nil, errors.New("must have email address to serve HTTPS without existing certificate and key") return nil, errors.New("must have email address to serve HTTPS without existing certificate and key")
...@@ -78,6 +99,20 @@ func groupConfigsByEmail(configs []server.Config) (map[string][]*server.Config, ...@@ -78,6 +99,20 @@ func groupConfigsByEmail(configs []server.Config) (map[string][]*server.Config,
return initMap, nil return initMap, nil
} }
// existingCertAndKey returns true if the host has a certificate
// and private key in storage already, false otherwise.
func existingCertAndKey(host string) bool {
_, err := os.Stat(storage.SiteCertFile(host))
if err != nil {
return false
}
_, err = os.Stat(storage.SiteKeyFile(host))
if err != nil {
return false
}
return true
}
// newClient creates a new ACME client to facilitate communication // newClient creates a new ACME client to facilitate communication
// with the Let's Encrypt CA server on behalf of the user specified // with the Let's Encrypt CA server on behalf of the user specified
// by leEmail. As part of this process, a user will be loaded from // by leEmail. As part of this process, a user will be loaded from
...@@ -168,33 +203,34 @@ func saveCertsAndKeys(certificates []acme.CertificateResource) error { ...@@ -168,33 +203,34 @@ func saveCertsAndKeys(certificates []acme.CertificateResource) error {
return nil return nil
} }
// autoConfigure enables TLS on all the configs in serverConfigs // autoConfigure enables TLS on cfg and appends, if necessary, a new config
// and appends, if necessary, new configs to allConfigs that redirect // to allConfigs that redirects plaintext HTTP to its new HTTPS counterpart.
// plaintext HTTP to their HTTPS counterparts. func autoConfigure(cfg *server.Config, allConfigs []server.Config) []server.Config {
func autoConfigure(allConfigs []server.Config, serverConfigs []*server.Config) []server.Config { cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host)
for _, cfg := range serverConfigs { cfg.TLS.Key = storage.SiteKeyFile(cfg.Host)
cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host) cfg.TLS.Enabled = true
cfg.TLS.Key = storage.SiteKeyFile(cfg.Host) cfg.Port = "https"
cfg.TLS.Enabled = true
cfg.Port = "https" // Is there a plaintext HTTP config for the same host? If not, make
// one and have it redirect all requests to this HTTPS host.
// Is there a plaintext HTTP config for the same host? If not, make if !hostHasOtherScheme(cfg.Host, "http", allConfigs) {
// one and have it redirect all requests to this HTTPS host. // Make one that redirects to HTTPS for all requests
var plaintextHostFound bool allConfigs = append(allConfigs, redirPlaintextHost(*cfg))
for _, otherCfg := range allConfigs { }
if cfg.Host == otherCfg.Host && otherCfg.Port == "http" { return allConfigs
plaintextHostFound = true }
break
}
}
if !plaintextHostFound { // hostHasOtherScheme tells you whether there is another config in the list
// Make one that redirects to HTTPS for all requests // for the same host but with the port equal to scheme. For example, to see
allConfigs = append(allConfigs, redirPlaintextHost(*cfg)) // if example.com has a https variant already, pass in example.com and
// "https" along with the list of configs.
func hostHasOtherScheme(host, scheme string, allConfigs []server.Config) bool {
for _, otherCfg := range allConfigs {
if otherCfg.Host == host && otherCfg.Port == scheme {
return true
} }
} }
return false
return allConfigs
} }
// redirPlaintextHost returns a new plaintext HTTP configuration for // redirPlaintextHost returns a new plaintext HTTP configuration for
......
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