update repo

This commit is contained in:
ministicraft
2019-06-04 22:58:42 +02:00
parent b5a7116849
commit 8ed9113bb2
62 changed files with 3810 additions and 82 deletions

View File

@@ -0,0 +1,12 @@
package dumper
// BaseConfig Base dump command configuration.
type BaseConfig struct {
DumpPath string
CrtInfo FileInfo
KeyInfo FileInfo
DomainSubDir bool
Clean bool
Watch bool
Hook string
}

View File

@@ -0,0 +1,129 @@
package dumper
import (
"encoding/pem"
"io/ioutil"
"os"
"path/filepath"
"github.com/go-acme/lego/certcrypto"
)
const (
certsSubDir = "certs"
keysSubDir = "private"
)
// FileInfo File information.
type FileInfo struct {
Name string
Ext string
}
// Dump Dumps data to certificates.
func Dump(data *StoredData, baseConfig *BaseConfig) error {
if baseConfig.Clean {
err := cleanDir(baseConfig.DumpPath)
if err != nil {
return err
}
}
if !baseConfig.DomainSubDir {
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, certsSubDir), 0755); err != nil {
return err
}
}
if err := os.MkdirAll(filepath.Join(baseConfig.DumpPath, keysSubDir), 0755); err != nil {
return err
}
privateKeyPem := extractPEMPrivateKey(data.Account)
err := ioutil.WriteFile(filepath.Join(baseConfig.DumpPath, keysSubDir, "letsencrypt"+baseConfig.KeyInfo.Ext), privateKeyPem, 0600)
if err != nil {
return err
}
for _, cert := range data.Certificates {
err := writeCert(baseConfig.DumpPath, cert, baseConfig.CrtInfo, baseConfig.DomainSubDir)
if err != nil {
return err
}
err = writeKey(baseConfig.DumpPath, cert, baseConfig.KeyInfo, baseConfig.DomainSubDir)
if err != nil {
return err
}
}
return nil
}
func writeCert(dumpPath string, cert *Certificate, info FileInfo, domainSubDir bool) error {
certPath := filepath.Join(dumpPath, certsSubDir, safeName(cert.Domain.Main+info.Ext))
if domainSubDir {
certPath = filepath.Join(dumpPath, safeName(cert.Domain.Main), info.Name+info.Ext)
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0755); err != nil {
return err
}
}
return ioutil.WriteFile(certPath, cert.Certificate, 0666)
}
func writeKey(dumpPath string, cert *Certificate, info FileInfo, domainSubDir bool) error {
keyPath := filepath.Join(dumpPath, keysSubDir, safeName(cert.Domain.Main+info.Ext))
if domainSubDir {
keyPath = filepath.Join(dumpPath, safeName(cert.Domain.Main), info.Name+info.Ext)
if err := os.MkdirAll(filepath.Join(dumpPath, safeName(cert.Domain.Main)), 0755); err != nil {
return err
}
}
return ioutil.WriteFile(keyPath, cert.Key, 0600)
}
func extractPEMPrivateKey(account *Account) []byte {
var block *pem.Block
switch account.KeyType {
case certcrypto.RSA2048, certcrypto.RSA4096, certcrypto.RSA8192:
block = &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: account.PrivateKey,
}
case certcrypto.EC256, certcrypto.EC384:
block = &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: account.PrivateKey,
}
default:
panic("unsupported key type")
}
return pem.EncodeToMemory(block)
}
func cleanDir(dumpPath string) error {
_, errExists := os.Stat(dumpPath)
if os.IsNotExist(errExists) {
return nil
}
if errExists != nil {
return errExists
}
dir, err := ioutil.ReadDir(dumpPath)
if err != nil {
return err
}
for _, f := range dir {
if err := os.RemoveAll(filepath.Join(dumpPath, f.Name())); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,167 @@
package file
import (
"bytes"
"crypto/md5"
"encoding/json"
"io"
"log"
"os"
"strings"
"github.com/fsnotify/fsnotify"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/hook"
)
// Dump Dumps "acme.json" file to certificates.
func Dump(acmeFile string, baseConfig *dumper.BaseConfig) error {
err := dump(acmeFile, baseConfig)
if err != nil {
return err
}
if baseConfig.Watch {
return watch(acmeFile, baseConfig)
}
return nil
}
func dump(acmeFile string, baseConfig *dumper.BaseConfig) error {
data, err := readFile(acmeFile)
if err != nil {
return err
}
return dumper.Dump(data, baseConfig)
}
func readFile(acmeFile string) (*dumper.StoredData, error) {
source, err := os.Open(acmeFile)
if err != nil {
return nil, err
}
data := &dumper.StoredData{}
if err = json.NewDecoder(source).Decode(data); err != nil {
return nil, err
}
return data, nil
}
func watch(acmeFile string, baseConfig *dumper.BaseConfig) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
defer func() { _ = watcher.Close() }()
done := make(chan bool)
go func() {
var previousHash []byte
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if isDebug() {
log.Println("event:", event)
}
hash, errW := manageEvent(watcher, event, acmeFile, previousHash, baseConfig)
if errW != nil {
log.Println("error:", errW)
done <- true
return
}
previousHash = hash
case errW, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", errW)
done <- true
return
}
}
}()
err = watcher.Add(acmeFile)
if err != nil {
return err
}
<-done
return nil
}
func manageEvent(watcher *fsnotify.Watcher, event fsnotify.Event, acmeFile string, previousHash []byte, baseConfig *dumper.BaseConfig) ([]byte, error) {
err := manageRename(watcher, event, acmeFile)
if err != nil {
return nil, err
}
hash, err := calculateHash(acmeFile)
if err != nil {
return nil, err
}
if !bytes.Equal(previousHash, hash) {
if isDebug() {
log.Println("detected changes on file:", event.Name)
}
if errD := dump(acmeFile, baseConfig); errD != nil {
return nil, errD
}
if isDebug() {
log.Println("Dumped new certificate data.")
}
hook.Exec(baseConfig.Hook)
}
return hash, nil
}
func manageRename(watcher *fsnotify.Watcher, event fsnotify.Event, acmeFile string) error {
if event.Op&fsnotify.Rename != fsnotify.Rename {
return nil
}
if err := watcher.Remove(acmeFile); err != nil {
return err
}
return watcher.Add(acmeFile)
}
func calculateHash(acmeFile string) ([]byte, error) {
file, err := os.Open(acmeFile)
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
h := md5.New()
_, err = io.Copy(h, file)
if err != nil {
return nil, err
}
return h.Sum(nil), nil
}
func isDebug() bool {
return strings.EqualFold(os.Getenv("TCD_DEBUG"), "true")
}

View File

@@ -0,0 +1,7 @@
// +build !windows
package dumper
func safeName(filename string) string {
return filename
}

View File

@@ -0,0 +1,9 @@
// +build windows
package dumper
import "strings"
func safeName(filename string) string {
return strings.ReplaceAll(filename, "*", "_")
}

View File

@@ -0,0 +1,35 @@
package dumper
import (
"github.com/go-acme/lego/certcrypto"
"github.com/go-acme/lego/registration"
)
// StoredData represents the data managed by the Store
type StoredData struct {
Account *Account
Certificates []*Certificate
HTTPChallenges map[string]map[string][]byte
TLSChallenges map[string]*Certificate
}
// Certificate is a struct which contains all data needed from an ACME certificate
type Certificate struct {
Domain Domain
Certificate []byte
Key []byte
}
// Domain holds a domain name with SANs
type Domain struct {
Main string
SANs []string
}
// Account is used to store lets encrypt registration info
type Account struct {
Email string
Registration *registration.Resource
PrivateKey []byte
KeyType certcrypto.KeyType
}

View File

@@ -0,0 +1,11 @@
package kv
import "github.com/abronan/valkeyrie/store"
// Config KV configuration.
type Config struct {
Backend store.Backend
Prefix string
Endpoints []string
Options *store.Config
}

View File

@@ -0,0 +1,65 @@
package kv
import (
"github.com/go-acme/lego/certcrypto"
"github.com/go-acme/lego/registration"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
)
// CertificateV1 is used to store certificate info
type CertificateV1 struct {
Domain string
CertURL string
CertStableURL string
PrivateKey []byte
Certificate []byte
}
// AccountV1 is used to store lets encrypt registration info
type AccountV1 struct {
Email string
Registration *registration.Resource
PrivateKey []byte
KeyType certcrypto.KeyType
DomainsCertificate DomainsCertificates
ChallengeCerts map[string]*ChallengeCert
HTTPChallenge map[string]map[string][]byte
}
// DomainsCertificates stores a certificate for multiple domains
type DomainsCertificates struct {
Certs []*DomainsCertificate
}
// ChallengeCert stores a challenge certificate
type ChallengeCert struct {
Certificate []byte
PrivateKey []byte
}
// DomainsCertificate contains a certificate for multiple domains
type DomainsCertificate struct {
Domains dumper.Domain
Certificate *CertificateV1
}
// convertAccountV1ToV2 converts account information from version 1 to 2
func convertAccountV1ToV2(account *AccountV1) *dumper.StoredData {
storedData := &dumper.StoredData{}
storedData.Account = &dumper.Account{
PrivateKey: account.PrivateKey,
Registration: account.Registration,
Email: account.Email,
KeyType: account.KeyType,
}
var certs []*dumper.Certificate
for _, oldCert := range account.DomainsCertificate.Certs {
certs = append(certs, &dumper.Certificate{
Certificate: oldCert.Certificate.Certificate,
Domain: oldCert.Domains,
Key: oldCert.Certificate.PrivateKey,
})
}
storedData.Certificates = certs
return storedData
}

View File

@@ -0,0 +1,99 @@
package kv
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"github.com/abronan/valkeyrie"
"github.com/abronan/valkeyrie/store"
"github.com/ldez/traefik-certs-dumper/v2/dumper"
"github.com/ldez/traefik-certs-dumper/v2/hook"
)
const storeKeySuffix = "/acme/account/object"
// Dump Dumps KV content to certificates.
func Dump(config *Config, baseConfig *dumper.BaseConfig) error {
kvStore, err := valkeyrie.NewStore(config.Backend, config.Endpoints, config.Options)
if err != nil {
return fmt.Errorf("unable to create client of the store: %v", err)
}
storeKey := config.Prefix + storeKeySuffix
if baseConfig.Watch {
return watch(kvStore, storeKey, baseConfig)
}
pair, err := kvStore.Get(storeKey, nil)
if err != nil {
return fmt.Errorf("unable to retrieve %s value: %v", storeKey, err)
}
return dumpPair(pair, baseConfig)
}
func watch(kvStore store.Store, storeKey string, baseConfig *dumper.BaseConfig) error {
stopCh := make(<-chan struct{})
pairs, err := kvStore.Watch(storeKey, stopCh, nil)
if err != nil {
return err
}
for {
pair := <-pairs
if pair == nil {
return fmt.Errorf("could not fetch Key/Value pair for key %v", storeKey)
}
err = dumpPair(pair, baseConfig)
if err != nil {
return err
}
if isDebug() {
log.Println("Dumped new certificate data.")
}
hook.Exec(baseConfig.Hook)
}
}
func dumpPair(pair *store.KVPair, baseConfig *dumper.BaseConfig) error {
data, err := getStoredDataFromGzip(pair)
if err != nil {
return err
}
return dumper.Dump(data, baseConfig)
}
func getStoredDataFromGzip(pair *store.KVPair) (*dumper.StoredData, error) {
reader, err := gzip.NewReader(bytes.NewBuffer(pair.Value))
if err != nil {
return nil, fmt.Errorf("fail to create GZip reader: %v", err)
}
acmeData, err := ioutil.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("unable to read the pair content: %v", err)
}
account := &AccountV1{}
if err := json.Unmarshal(acmeData, &account); err != nil {
return nil, fmt.Errorf("unable marshal AccountV1: %v", err)
}
return convertAccountV1ToV2(account), nil
}
func isDebug() bool {
return strings.EqualFold(os.Getenv("TCD_DEBUG"), "true")
}