Commit c626774d authored by xenolf's avatar xenolf

First, raw renewal implementation. Pretty basic :D

parent cd0b47d0
......@@ -7,9 +7,11 @@ import (
"encoding/json"
"errors"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/redirect"
......@@ -38,6 +40,8 @@ func Activate(configs []server.Config) ([]server.Config, error) {
configs = autoConfigure(&configs[i], configs)
}
}
// Handle cert renewal on Startup
processCertificateRenewal(configs)
// Group configs by LE email address; this will help us
// reduce round-trips when getting the certs.
......@@ -73,6 +77,8 @@ func Activate(configs []server.Config) ([]server.Config, error) {
}
}
go renewalFunc(configs)
return configs, nil
}
......@@ -211,7 +217,7 @@ func saveCertsAndKeys(certificates []acme.CertificateResource) error {
}
// Save cert metadata
jsonBytes, err := json.MarshalIndent(&CertificateMeta{URL: cert.CertURL, Domain: cert.Domain}, "", "\t")
jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
if err != nil {
return err
}
......@@ -278,6 +284,141 @@ 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
......@@ -294,6 +435,9 @@ const (
// The port to expose to the CA server for Simple HTTP Challenge
exposePort = "5001"
// Renewal Timer - Check renewals every x hours.
renewTimer = 24
)
// KeySize represents the length of a key in bits.
......
......@@ -16,6 +16,11 @@ 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")
......
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