mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-30 18:22:49 -04:00 
			
		
		
		
	Simplify timing mechanism for checking renewals
This commit is contained in:
		
							parent
							
								
									c626774da2
								
							
						
					
					
						commit
						38885e4301
					
				| @ -7,7 +7,6 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| @ -40,7 +39,8 @@ func Activate(configs []server.Config) ([]server.Config, error) { | ||||
| 			configs = autoConfigure(&configs[i], configs) | ||||
| 		} | ||||
| 	} | ||||
| 	// Handle cert renewal on Startup | ||||
| 
 | ||||
| 	// First renew any existing certificates that need it | ||||
| 	processCertificateRenewal(configs) | ||||
| 
 | ||||
| 	// 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 | ||||
| } | ||||
| @ -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 ( | ||||
| 	// Let's Encrypt account email to use if none provided | ||||
| 	DefaultEmail string | ||||
| @ -431,13 +296,14 @@ var ( | ||||
| const ( | ||||
| 	// The base URL to the Let's Encrypt CA | ||||
| 	// 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" | ||||
| 
 | ||||
| 	// The port to expose to the CA server for Simple HTTP Challenge | ||||
| 	exposePort = "5001" | ||||
| 
 | ||||
| 	// Renewal Timer - Check renewals every x hours. | ||||
| 	renewTimer = 24 | ||||
| 	// How often to check certificates for renewal | ||||
| 	renewInterval = 24 * time.Hour | ||||
| ) | ||||
| 
 | ||||
| // 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. | ||||
| 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. | ||||
| func (s Storage) Sites() string { | ||||
| 	return filepath.Join(string(s), "sites") | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user