Persist, renew and delete sessions, refactor storage package, move reusable packages to pkg
This commit is contained in:
parent
121582f72a
commit
24f9553aa5
48 changed files with 1872 additions and 1171 deletions
38
pkg/letsencrypt/directory.go
Normal file
38
pkg/letsencrypt/directory.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package letsencrypt
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Directory string
|
||||
|
||||
func (d Directory) Domain(domain string) string {
|
||||
return filepath.Join(string(d), "certs", domain)
|
||||
}
|
||||
|
||||
func (d Directory) Cert(domain string) string {
|
||||
return filepath.Join(d.Domain(domain), "cert.pem")
|
||||
}
|
||||
|
||||
func (d Directory) Key(domain string) string {
|
||||
return filepath.Join(d.Domain(domain), "key.pem")
|
||||
}
|
||||
|
||||
func (d Directory) Meta(domain string) string {
|
||||
return filepath.Join(d.Domain(domain), "metadata.json")
|
||||
}
|
||||
|
||||
func (d Directory) User(email string) string {
|
||||
if email == "" {
|
||||
email = defaultUser
|
||||
}
|
||||
return filepath.Join(string(d), "users", email)
|
||||
}
|
||||
|
||||
func (d Directory) UserRegistration(email string) string {
|
||||
return filepath.Join(d.User(email), "registration.json")
|
||||
}
|
||||
|
||||
func (d Directory) UserKey(email string) string {
|
||||
return filepath.Join(d.User(email), "key.pem")
|
||||
}
|
287
pkg/letsencrypt/letsencrypt.go
Normal file
287
pkg/letsencrypt/letsencrypt.go
Normal file
|
@ -0,0 +1,287 @@
|
|||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
109
pkg/letsencrypt/user.go
Normal file
109
pkg/letsencrypt/user.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package letsencrypt
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
const defaultUser = "default"
|
||||
|
||||
type User struct {
|
||||
Email string
|
||||
Registration *acme.RegistrationResource
|
||||
key crypto.PrivateKey
|
||||
}
|
||||
|
||||
func (u User) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
func (u User) GetRegistration() *acme.RegistrationResource {
|
||||
return u.Registration
|
||||
}
|
||||
|
||||
func (u User) GetPrivateKey() crypto.PrivateKey {
|
||||
return u.key
|
||||
}
|
||||
|
||||
func newUser(email string) (User, error) {
|
||||
var err error
|
||||
user := User{Email: email}
|
||||
user.key, err = rsa.GenerateKey(rand.Reader, KeySize)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func getUser(email string) (User, error) {
|
||||
var user User
|
||||
|
||||
reg, err := os.Open(directory.UserRegistration(email))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return newUser(email)
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
defer reg.Close()
|
||||
|
||||
err = json.NewDecoder(reg).Decode(&user)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
user.key, err = loadRSAPrivateKey(directory.UserKey(email))
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func saveUser(user User) error {
|
||||
err := os.MkdirAll(directory.User(user.Email), 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = saveRSAPrivateKey(user.key, directory.UserKey(user.Email))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jsonBytes, err := json.MarshalIndent(&user, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(directory.UserRegistration(user.Email), jsonBytes, 0600)
|
||||
}
|
||||
|
||||
func loadRSAPrivateKey(file string) (crypto.PrivateKey, error) {
|
||||
keyBytes, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyBlock, _ := pem.Decode(keyBytes)
|
||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
}
|
||||
|
||||
func saveRSAPrivateKey(key crypto.PrivateKey, file string) error {
|
||||
pemKey := pem.Block{
|
||||
Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key.(*rsa.PrivateKey)),
|
||||
}
|
||||
keyOut, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer keyOut.Close()
|
||||
return pem.Encode(keyOut, &pemKey)
|
||||
}
|
35
pkg/letsencrypt/user_test.go
Normal file
35
pkg/letsencrypt/user_test.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package letsencrypt
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func tempdir() string {
|
||||
f, _ := ioutil.TempDir("", "")
|
||||
return f
|
||||
}
|
||||
|
||||
func testUser(t *testing.T, email string) {
|
||||
user, err := newUser(email)
|
||||
assert.Nil(t, err)
|
||||
key := user.GetPrivateKey()
|
||||
assert.NotNil(t, key)
|
||||
|
||||
err = saveUser(user)
|
||||
assert.Nil(t, err)
|
||||
|
||||
user, err = getUser(email)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, email, user.GetEmail())
|
||||
assert.Equal(t, key, user.GetPrivateKey())
|
||||
}
|
||||
|
||||
func TestUser(t *testing.T) {
|
||||
directory = Directory(tempdir())
|
||||
|
||||
testUser(t, "test@test.com")
|
||||
testUser(t, "")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue