mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-04 03:27:23 -05:00 
			
		
		
		
	Simplify timing mechanism for checking renewals
This commit is contained in:
		
							parent
							
								
									c626774da2
								
							
						
					
					
						commit
						38885e4301
					
				@ -7,7 +7,6 @@ import (
 | 
				
			|||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
@ -40,7 +39,8 @@ func Activate(configs []server.Config) ([]server.Config, error) {
 | 
				
			|||||||
			configs = autoConfigure(&configs[i], configs)
 | 
								configs = autoConfigure(&configs[i], configs)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// Handle cert renewal on Startup
 | 
					
 | 
				
			||||||
 | 
						// First renew any existing certificates that need it
 | 
				
			||||||
	processCertificateRenewal(configs)
 | 
						processCertificateRenewal(configs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Group configs by LE email address; this will help us
 | 
						// Group configs by LE email address; this will help us
 | 
				
			||||||
@ -77,7 +77,7 @@ func Activate(configs []server.Config) ([]server.Config, error) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	go renewalFunc(configs)
 | 
						go keepCertificatesRenewed(configs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return configs, nil
 | 
						return configs, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -284,141 +284,6 @@ func redirPlaintextHost(cfg server.Config) server.Config {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func renewalFunc(configs []server.Config) {
 | 
					 | 
				
			||||||
	nextRun, err := processCertificateRenewal(configs)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Printf("[ERROR] Could not start renewal routine. %v", err)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for {
 | 
					 | 
				
			||||||
		timer := time.NewTimer(time.Duration(nextRun) * time.Hour)
 | 
					 | 
				
			||||||
		<-timer.C
 | 
					 | 
				
			||||||
		nextRun, err = processCertificateRenewal(configs)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Printf("[ERROR] Renewal routing stopped. %v", err)
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// checkCertificateRenewal loops through all configured
 | 
					 | 
				
			||||||
// sites and looks for certificates to renew. Nothing is mutated
 | 
					 | 
				
			||||||
// through this function. The changes happen directly on disk.
 | 
					 | 
				
			||||||
func processCertificateRenewal(configs []server.Config) (int, error) {
 | 
					 | 
				
			||||||
	log.Print("[INFO] Processing certificate renewals...")
 | 
					 | 
				
			||||||
	// Check if we should run. If not, get out of here.
 | 
					 | 
				
			||||||
	next, err := getNextRenewalShedule()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return 0, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if next > 0 {
 | 
					 | 
				
			||||||
		return next, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// We are executing. Write the current timestamp into the file.
 | 
					 | 
				
			||||||
	err = ioutil.WriteFile(storage.RenewTimerFile(), []byte(time.Now().UTC().Format(time.RFC3339)), 0600)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return 0, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	next = renewTimer
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, cfg := range configs {
 | 
					 | 
				
			||||||
		// Check if this entry is TLS enabled and managed by LE
 | 
					 | 
				
			||||||
		if !cfg.TLS.Enabled || !existingCertAndKey(cfg.Host) {
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Read the certificate and get the NotAfter time.
 | 
					 | 
				
			||||||
		certBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host))
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return 0, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		expTime, err := acme.GetPEMCertExpiration(certBytes)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return 0, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// The time returned from the certificate is always in UTC.
 | 
					 | 
				
			||||||
		// So calculate the time left with local time as UTC.
 | 
					 | 
				
			||||||
		// Directly convert it to days for the following checks.
 | 
					 | 
				
			||||||
		daysLeft := int(expTime.Sub(time.Now().UTC()).Hours() / 24)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Renew on two or less days remaining.
 | 
					 | 
				
			||||||
		if daysLeft <= 2 {
 | 
					 | 
				
			||||||
			log.Printf("[WARN] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host)
 | 
					 | 
				
			||||||
			client, err := newClient(getEmail(cfg))
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return 0, err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Read metadata
 | 
					 | 
				
			||||||
			metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(cfg.Host))
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return 0, err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			privBytes, err := ioutil.ReadFile(storage.SiteKeyFile(cfg.Host))
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return 0, err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			var certMeta acme.CertificateResource
 | 
					 | 
				
			||||||
			err = json.Unmarshal(metaBytes, &certMeta)
 | 
					 | 
				
			||||||
			certMeta.Certificate = certBytes
 | 
					 | 
				
			||||||
			certMeta.PrivateKey = privBytes
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Renew certificate.
 | 
					 | 
				
			||||||
			// TODO: revokeOld should be an option in the caddyfile
 | 
					 | 
				
			||||||
			newCertMeta, err := client.RenewCertificate(certMeta, true)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return 0, err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			saveCertsAndKeys([]acme.CertificateResource{newCertMeta})
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Warn on 14 days remaining
 | 
					 | 
				
			||||||
		if daysLeft <= 14 {
 | 
					 | 
				
			||||||
			log.Printf("[WARN] There are %d days left on the certificate of %s. Will renew on two days left.\n", daysLeft, cfg.Host)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return next, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// getNextRenewalShedule calculates the offset in hours the renew process should
 | 
					 | 
				
			||||||
// run from the current time. If the file the time is in does not exists, the
 | 
					 | 
				
			||||||
// function returns zero to trigger a renew asap.
 | 
					 | 
				
			||||||
func getNextRenewalShedule() (int, error) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if the file exists. If it does not, return 0 to indicate immediate processing.
 | 
					 | 
				
			||||||
	if _, err := os.Stat(storage.RenewTimerFile()); os.IsNotExist(err) {
 | 
					 | 
				
			||||||
		return 0, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	renewTimeBytes, err := ioutil.ReadFile(storage.RenewTimerFile())
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return 0, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	renewalTime, err := time.Parse(time.RFC3339, string(renewTimeBytes))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return 0, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// The time read from the file was equal or more then 24 hours in the past,
 | 
					 | 
				
			||||||
	// write the current time to the file and return true.
 | 
					 | 
				
			||||||
	hoursSinceRenew := int(time.Now().UTC().Sub(renewalTime).Hours())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if hoursSinceRenew >= renewTimer {
 | 
					 | 
				
			||||||
		return 0, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return hoursSinceRenew, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	// Let's Encrypt account email to use if none provided
 | 
						// Let's Encrypt account email to use if none provided
 | 
				
			||||||
	DefaultEmail string
 | 
						DefaultEmail string
 | 
				
			||||||
@ -431,13 +296,14 @@ var (
 | 
				
			|||||||
const (
 | 
					const (
 | 
				
			||||||
	// The base URL to the Let's Encrypt CA
 | 
						// The base URL to the Let's Encrypt CA
 | 
				
			||||||
	// TODO: Staging API URL is: https://acme-staging.api.letsencrypt.org
 | 
						// TODO: Staging API URL is: https://acme-staging.api.letsencrypt.org
 | 
				
			||||||
 | 
						// TODO: Production endpoint is: https://acme-v01.api.letsencrypt.org
 | 
				
			||||||
	caURL = "http://192.168.99.100:4000"
 | 
						caURL = "http://192.168.99.100:4000"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// The port to expose to the CA server for Simple HTTP Challenge
 | 
						// The port to expose to the CA server for Simple HTTP Challenge
 | 
				
			||||||
	exposePort = "5001"
 | 
						exposePort = "5001"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Renewal Timer - Check renewals every x hours.
 | 
						// How often to check certificates for renewal
 | 
				
			||||||
	renewTimer = 24
 | 
						renewInterval = 24 * time.Hour
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// KeySize represents the length of a key in bits.
 | 
					// KeySize represents the length of a key in bits.
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										94
									
								
								config/letsencrypt/renew.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								config/letsencrypt/renew.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,94 @@
 | 
				
			|||||||
 | 
					package letsencrypt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/mholt/caddy/server"
 | 
				
			||||||
 | 
						"github.com/xenolf/lego/acme"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// keepCertificatesRenewed is a permanently-blocking function
 | 
				
			||||||
 | 
					// that loops indefinitely and, on a regular schedule, checks
 | 
				
			||||||
 | 
					// certificates for expiration and initiates a renewal of certs
 | 
				
			||||||
 | 
					// that are expiring soon.
 | 
				
			||||||
 | 
					func keepCertificatesRenewed(configs []server.Config) {
 | 
				
			||||||
 | 
						ticker := time.Tick(renewInterval)
 | 
				
			||||||
 | 
						for range ticker {
 | 
				
			||||||
 | 
							if err := processCertificateRenewal(configs); err != nil {
 | 
				
			||||||
 | 
								log.Printf("[ERROR] cert renewal: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// checkCertificateRenewal loops through all configured
 | 
				
			||||||
 | 
					// sites and looks for certificates to renew. Nothing is mutated
 | 
				
			||||||
 | 
					// through this function. The changes happen directly on disk.
 | 
				
			||||||
 | 
					func processCertificateRenewal(configs []server.Config) error {
 | 
				
			||||||
 | 
						log.Print("[INFO] Processing certificate renewals...")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, cfg := range configs {
 | 
				
			||||||
 | 
							// Check if this entry is TLS enabled and managed by LE
 | 
				
			||||||
 | 
							if !cfg.TLS.Enabled || !existingCertAndKey(cfg.Host) {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Read the certificate and get the NotAfter time.
 | 
				
			||||||
 | 
							certBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							expTime, err := acme.GetPEMCertExpiration(certBytes)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// The time returned from the certificate is always in UTC.
 | 
				
			||||||
 | 
							// So calculate the time left with local time as UTC.
 | 
				
			||||||
 | 
							// Directly convert it to days for the following checks.
 | 
				
			||||||
 | 
							daysLeft := int(expTime.Sub(time.Now().UTC()).Hours() / 24)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Renew on two or less days remaining.
 | 
				
			||||||
 | 
							if daysLeft <= 2 {
 | 
				
			||||||
 | 
								log.Printf("[WARN] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host)
 | 
				
			||||||
 | 
								client, err := newClient(getEmail(cfg))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Read metadata
 | 
				
			||||||
 | 
								metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(cfg.Host))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								privBytes, err := ioutil.ReadFile(storage.SiteKeyFile(cfg.Host))
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var certMeta acme.CertificateResource
 | 
				
			||||||
 | 
								err = json.Unmarshal(metaBytes, &certMeta)
 | 
				
			||||||
 | 
								certMeta.Certificate = certBytes
 | 
				
			||||||
 | 
								certMeta.PrivateKey = privBytes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Renew certificate.
 | 
				
			||||||
 | 
								// TODO: revokeOld should be an option in the caddyfile
 | 
				
			||||||
 | 
								newCertMeta, err := client.RenewCertificate(certMeta, true)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								saveCertsAndKeys([]acme.CertificateResource{newCertMeta})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Warn on 14 days remaining
 | 
				
			||||||
 | 
							if daysLeft <= 14 {
 | 
				
			||||||
 | 
								log.Printf("[WARN] There are %d days left on the certificate of %s. Will renew on two days left.\n", daysLeft, cfg.Host)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -16,11 +16,6 @@ var storage = Storage(filepath.Join(app.DataFolder(), "letsencrypt"))
 | 
				
			|||||||
// forming file paths derived from it.
 | 
					// forming file paths derived from it.
 | 
				
			||||||
type Storage string
 | 
					type Storage string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RenewTimerFile returns the path to the file used for renewal timing.
 | 
					 | 
				
			||||||
func (s Storage) RenewTimerFile() string {
 | 
					 | 
				
			||||||
	return filepath.Join(string(s), "lastrenew")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Sites gets the directory that stores site certificate and keys.
 | 
					// Sites gets the directory that stores site certificate and keys.
 | 
				
			||||||
func (s Storage) Sites() string {
 | 
					func (s Storage) Sites() string {
 | 
				
			||||||
	return filepath.Join(string(s), "sites")
 | 
						return filepath.Join(string(s), "sites")
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user