285 lines
4.8 KiB
Go
285 lines
4.8 KiB
Go
package letsencrypt
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"io/ioutil"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"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) (*state, error) {
|
|
directory = Directory(dir)
|
|
|
|
user, err := getUser(email)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client, err := acme.NewClient(URL, &user, acme.RSA2048)
|
|
client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01})
|
|
client.SetHTTPAddress(port)
|
|
|
|
if user.Registration == nil {
|
|
user.Registration, err = client.Register()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = client.AgreeToTOS()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = saveUser(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
s := &state{
|
|
client: client,
|
|
domain: domain,
|
|
}
|
|
|
|
if certExists(domain) {
|
|
if !s.renew() {
|
|
err = s.loadCert()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
s.refreshOCSP()
|
|
} else {
|
|
err = s.obtain()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
go s.maintain()
|
|
|
|
return s, nil
|
|
}
|
|
|
|
type state struct {
|
|
client *acme.Client
|
|
domain string
|
|
cert *tls.Certificate
|
|
certPEM []byte
|
|
lock sync.Mutex
|
|
}
|
|
|
|
func (s *state) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
s.lock.Lock()
|
|
cert := s.cert
|
|
s.lock.Unlock()
|
|
|
|
return cert, nil
|
|
}
|
|
|
|
func (s *state) getCertPEM() []byte {
|
|
s.lock.Lock()
|
|
certPEM := s.certPEM
|
|
s.lock.Unlock()
|
|
|
|
return certPEM
|
|
}
|
|
|
|
func (s *state) setCert(meta acme.CertificateResource) {
|
|
cert, err := tls.X509KeyPair(meta.Certificate, meta.PrivateKey)
|
|
if err == nil {
|
|
s.lock.Lock()
|
|
if s.cert != nil {
|
|
cert.OCSPStaple = s.cert.OCSPStaple
|
|
}
|
|
|
|
s.cert = &cert
|
|
s.certPEM = meta.Certificate
|
|
s.lock.Unlock()
|
|
}
|
|
}
|
|
|
|
func (s *state) setOCSP(ocsp []byte) {
|
|
cert := tls.Certificate{
|
|
OCSPStaple: ocsp,
|
|
}
|
|
|
|
s.lock.Lock()
|
|
if s.cert != nil {
|
|
cert.Certificate = s.cert.Certificate
|
|
cert.PrivateKey = s.cert.PrivateKey
|
|
}
|
|
s.cert = &cert
|
|
s.lock.Unlock()
|
|
}
|
|
|
|
func (s *state) obtain() error {
|
|
cert, errors := s.client.ObtainCertificate([]string{s.domain}, true, nil, false)
|
|
if err := errors[s.domain]; err != nil {
|
|
if _, ok := err.(acme.TOSError); ok {
|
|
err := s.client.AgreeToTOS()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.obtain()
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
s.setCert(cert)
|
|
s.refreshOCSP()
|
|
|
|
err := saveCert(cert)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *state) renew() bool {
|
|
cert, err := ioutil.ReadFile(directory.Cert(s.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(s.domain))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
key, err := ioutil.ReadFile(directory.Key(s.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 := s.client.RenewCertificate(meta, true, false)
|
|
if err != nil {
|
|
if _, ok := err.(acme.TOSError); ok {
|
|
err := s.client.AgreeToTOS()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
goto Renew
|
|
}
|
|
return false
|
|
}
|
|
|
|
s.setCert(newMeta)
|
|
|
|
err = saveCert(newMeta)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (s *state) refreshOCSP() {
|
|
ocsp, resp, err := acme.GetOCSPForCert(s.getCertPEM())
|
|
if err == nil && resp.Status == acme.OCSPGood {
|
|
s.setOCSP(ocsp)
|
|
}
|
|
}
|
|
|
|
func (s *state) maintain() {
|
|
renew := time.Tick(24 * time.Hour)
|
|
ocsp := time.Tick(1 * time.Hour)
|
|
for {
|
|
select {
|
|
case <-renew:
|
|
s.renew()
|
|
|
|
case <-ocsp:
|
|
s.refreshOCSP()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *state) loadCert() error {
|
|
cert, err := ioutil.ReadFile(directory.Cert(s.domain))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
key, err := ioutil.ReadFile(directory.Key(s.domain))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.setCert(acme.CertificateResource{
|
|
Certificate: cert,
|
|
PrivateKey: key,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|