dispatch/letsencrypt/letsencrypt.go
Ken-Håvard Lieng b55cb13e44 Let's Encrypt
2016-01-04 19:26:32 +01:00

184 lines
3.3 KiB
Go

package letsencrypt
import (
"encoding/json"
"io/ioutil"
"os"
"time"
"github.com/khlieng/dispatch/Godeps/_workspace/src/github.com/xenolf/lego/acme"
)
const URL = "https://acme-v01.api.letsencrypt.org/directory"
const KeySize = 2048
var directory Directory
func Run(dir, domain, email, port string, onChange func()) (string, string, error) {
directory = Directory(dir)
user, err := getUser(email)
if err != nil {
return "", "", nil
}
client, err := acme.NewClient(URL, &user, KeySize)
client.ExcludeChallenges([]string{"tls-sni-01"})
client.SetHTTPPort(port)
if user.Registration == nil {
user.Registration, err = client.Register()
if err != nil {
return "", "", err
}
err = client.AgreeToTOS()
if err != nil {
return "", "", err
}
err = saveUser(user)
if err != nil {
return "", "", err
}
}
if certExists(domain) {
renew(client, domain)
} else {
err = obtain(client, domain)
if err != nil {
return "", "", err
}
}
go keepRenewed(client, domain, onChange)
return directory.Cert(domain), directory.Key(domain), nil
}
func obtain(client *acme.Client, domain string) error {
cert, errors := client.ObtainCertificate([]string{domain}, false)
if err := errors[domain]; err != nil {
if _, ok := err.(acme.TOSError); ok {
err := client.AgreeToTOS()
if err != nil {
return err
}
return obtain(client, domain)
}
return err
}
err := saveCert(cert)
if err != nil {
return err
}
return nil
}
func renew(client *acme.Client, domain string) bool {
cert, err := ioutil.ReadFile(directory.Cert(domain))
if err != nil {
return false
}
exp, err := acme.GetPEMCertExpiration(cert)
if err != nil {
return false
}
daysLeft := int(exp.Sub(time.Now().UTC()).Hours() / 24)
if daysLeft <= 30 {
metaBytes, err := ioutil.ReadFile(directory.Meta(domain))
if err != nil {
return false
}
key, err := ioutil.ReadFile(directory.Key(domain))
if err != nil {
return false
}
var meta acme.CertificateResource
err = json.Unmarshal(metaBytes, &meta)
if err != nil {
return false
}
meta.Certificate = cert
meta.PrivateKey = key
Renew:
newMeta, err := client.RenewCertificate(meta, false)
if err != nil {
if _, ok := err.(acme.TOSError); ok {
err := client.AgreeToTOS()
if err != nil {
return false
}
goto Renew
}
return false
}
err = saveCert(newMeta)
if err != nil {
return false
}
return true
}
return false
}
func keepRenewed(client *acme.Client, domain string, onChange func()) {
for {
time.Sleep(24 * time.Hour)
if renew(client, domain) {
onChange()
}
}
}
func certExists(domain string) bool {
if _, err := os.Stat(directory.Cert(domain)); err != nil {
return false
}
if _, err := os.Stat(directory.Key(domain)); err != nil {
return false
}
return true
}
func saveCert(cert acme.CertificateResource) error {
err := os.MkdirAll(directory.Domain(cert.Domain), 0700)
if err != nil {
return err
}
err = ioutil.WriteFile(directory.Cert(cert.Domain), cert.Certificate, 0600)
if err != nil {
return err
}
err = ioutil.WriteFile(directory.Key(cert.Domain), cert.PrivateKey, 0600)
if err != nil {
return err
}
jsonBytes, err := json.MarshalIndent(&cert, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(directory.Meta(cert.Domain), jsonBytes, 0600)
if err != nil {
return err
}
return nil
}