Implementation of certmagic storage using micro's store and sync packages
This commit is contained in:
		| @@ -2,7 +2,7 @@ language: go | ||||
| go: | ||||
| - 1.13.x | ||||
| env: | ||||
|   - GO111MODULE=on | ||||
|   - GO111MODULE=on IN_TRAVIS_CI=yes | ||||
| notifications: | ||||
|   slack: | ||||
|     secure: aEvhLbhujaGaKSrOokiG3//PaVHTIrc3fBpoRbCRqfZpyq6WREoapJJhF+tIpWWOwaC9GmChbD6aHo/jMUgwKXVyPSaNjiEL87YzUUpL8B2zslNp1rgfTg/LrzthOx3Q1TYwpaAl3to0fuHUVFX4yMeC2vuThq7WSXgMMxFCtbc= | ||||
|   | ||||
| @@ -4,8 +4,6 @@ package acme | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"net" | ||||
|  | ||||
| 	"github.com/go-acme/lego/v3/challenge" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| @@ -24,62 +22,3 @@ const ( | ||||
| 	LetsEncryptStagingCA    = "https://acme-staging-v02.api.letsencrypt.org/directory" | ||||
| 	LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory" | ||||
| ) | ||||
|  | ||||
| // Option (or Options) are passed to New() to configure providers | ||||
| type Option func(o *Options) | ||||
|  | ||||
| // Options represents various options you can present to ACME providers | ||||
| type Options struct { | ||||
| 	// AcceptTLS must be set to true to indicate that you have read your | ||||
| 	// provider's terms of service. | ||||
| 	AcceptToS bool | ||||
| 	// CA is the CA to use | ||||
| 	CA string | ||||
| 	// ChallengeProvider is a go-acme/lego challenge provider. Set this if you | ||||
| 	// want to use DNS Challenges. Otherwise, tls-alpn-01 will be used | ||||
| 	ChallengeProvider challenge.Provider | ||||
| 	// Issue certificates for domains on demand. Otherwise, certs will be | ||||
| 	// retrieved / issued on start-up. | ||||
| 	OnDemand bool | ||||
| 	// TODO | ||||
| 	Cache interface{} | ||||
| } | ||||
|  | ||||
| // AcceptToS indicates whether you accept your CA's terms of service | ||||
| func AcceptToS(b bool) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.AcceptToS = b | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // CA sets the CA of an acme.Options | ||||
| func CA(CA string) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.CA = CA | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ChallengeProvider sets the Challenge provider of an acme.Options | ||||
| // if set, it enables the DNS challenge, otherwise tls-alpn-01 will be used. | ||||
| func ChallengeProvider(p challenge.Provider) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.ChallengeProvider = p | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // OnDemand enables on-demand certificate issuance. Not recommended for use | ||||
| // with the DNS challenge, as the first connection may be very slow. | ||||
| func OnDemand(b bool) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.OnDemand = b | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Default uses the Let's Encrypt Production CA, with DNS Challenge disabled. | ||||
| func Default() []Option { | ||||
| 	return []Option{ | ||||
| 		AcceptToS(true), | ||||
| 		CA(LetsEncryptProductionCA), | ||||
| 		OnDemand(true), | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| package certmagic | ||||
|  | ||||
| import ( | ||||
| 	"log" | ||||
| 	"net" | ||||
|  | ||||
| 	"github.com/mholt/certmagic" | ||||
| @@ -36,6 +37,12 @@ func New(options ...acme.Option) acme.Provider { | ||||
| 			op(o) | ||||
| 		} | ||||
| 	} | ||||
| 	if o.Cache != nil { | ||||
| 		if _, ok := o.Cache.(certmagic.Storage); !ok { | ||||
| 			log.Fatal("ACME: cache provided doesn't implement certmagic's Storage interface") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return &certmagicProvider{ | ||||
| 		opts: o, | ||||
| 	} | ||||
|   | ||||
| @@ -1,19 +1,24 @@ | ||||
| package certmagic | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/go-acme/lego/v3/providers/dns/cloudflare" | ||||
| 	// "github.com/micro/go-micro/api/server/acme" | ||||
| 	"github.com/mholt/certmagic" | ||||
| 	"github.com/micro/go-micro/api/server/acme" | ||||
| 	"github.com/micro/go-micro/sync/lock/memory" | ||||
| ) | ||||
|  | ||||
| func TestCertMagic(t *testing.T) { | ||||
| 	// TODO: Travis doesn't let us bind :443 | ||||
| 	// l, err := New().NewListener() | ||||
| 	// if err != nil { | ||||
| 	// 	t.Error(err.Error()) | ||||
| 	// } | ||||
| 	// l.Close() | ||||
| 	if len(os.Getenv("IN_TRAVIS_CI")) != 0 { | ||||
| 		t.Skip("Travis doesn't let us bind :443") | ||||
| 	} | ||||
| 	l, err := New().NewListener() | ||||
| 	if err != nil { | ||||
| 		t.Error(err.Error()) | ||||
| 	} | ||||
| 	l.Close() | ||||
|  | ||||
| 	c := cloudflare.NewDefaultConfig() | ||||
| 	c.AuthEmail = "" | ||||
| @@ -21,19 +26,30 @@ func TestCertMagic(t *testing.T) { | ||||
| 	c.AuthToken = "test" | ||||
| 	c.ZoneToken = "test" | ||||
|  | ||||
| 	_, err := cloudflare.NewDNSProviderConfig(c) | ||||
| 	p, err := cloudflare.NewDNSProviderConfig(c) | ||||
| 	if err != nil { | ||||
| 		t.Error(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	// TODO: Travis doesn't let us bind :443 | ||||
| 	// l, err = New(acme.AcceptTLS(true), | ||||
| 	// 	acme.CA(acme.LetsEncryptStagingCA), | ||||
| 	// 	acme.ChallengeProvider(p), | ||||
| 	// ).NewListener() | ||||
| 	l, err = New(acme.AcceptToS(true), | ||||
| 		acme.CA(acme.LetsEncryptStagingCA), | ||||
| 		acme.ChallengeProvider(p), | ||||
| 	).NewListener() | ||||
|  | ||||
| 	// if err != nil { | ||||
| 	// 	t.Error(err.Error()) | ||||
| 	// } | ||||
| 	// l.Close() | ||||
| 	if err != nil { | ||||
| 		t.Error(err.Error()) | ||||
| 	} | ||||
| 	l.Close() | ||||
| } | ||||
|  | ||||
| func TestStorageImplementation(t *testing.T) { | ||||
| 	var s certmagic.Storage | ||||
| 	s = &storage{ | ||||
| 		lock: memory.NewLock(), | ||||
| 	} | ||||
| 	if err := s.Lock("test"); err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
| 	s.Unlock("test") | ||||
| 	New(acme.Cache(s)) | ||||
| } | ||||
|   | ||||
							
								
								
									
										134
									
								
								api/server/acme/certmagic/storage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								api/server/acme/certmagic/storage.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| package certmagic | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/gob" | ||||
| 	"fmt" | ||||
| 	"path" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/mholt/certmagic" | ||||
| 	"github.com/micro/go-micro/store" | ||||
| 	"github.com/micro/go-micro/sync/lock" | ||||
| ) | ||||
|  | ||||
| // file represents a "file" that will be stored in store.Store - the contents and last modified time | ||||
| type file struct { | ||||
| 	// last modified time | ||||
| 	lastModified time.Time | ||||
| 	// contents | ||||
| 	contents []byte | ||||
| } | ||||
|  | ||||
| // storage is an implementation of certmagic.Storage using micro's sync.Map and store.Store interfaces. | ||||
| // As certmagic storage expects a filesystem (with stat() abilities) we have to implement | ||||
| // the bare minimum of metadata. | ||||
| type storage struct { | ||||
| 	lock  lock.Lock | ||||
| 	store store.Store | ||||
| } | ||||
|  | ||||
| func (s *storage) Lock(key string) error { | ||||
| 	return s.lock.Acquire(key, lock.TTL(10*time.Minute)) | ||||
| } | ||||
|  | ||||
| func (s *storage) Unlock(key string) error { | ||||
| 	return s.lock.Release(key) | ||||
| } | ||||
|  | ||||
| func (s *storage) Store(key string, value []byte) error { | ||||
| 	f := file{ | ||||
| 		lastModified: time.Now(), | ||||
| 		contents:     value, | ||||
| 	} | ||||
| 	buf := &bytes.Buffer{} | ||||
| 	e := gob.NewEncoder(buf) | ||||
| 	if err := e.Encode(f); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	r := &store.Record{ | ||||
| 		Key:   key, | ||||
| 		Value: buf.Bytes(), | ||||
| 	} | ||||
| 	return s.store.Write(r) | ||||
| } | ||||
|  | ||||
| func (s *storage) Load(key string) ([]byte, error) { | ||||
| 	records, err := s.store.Read(key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if len(records) != 1 { | ||||
| 		return nil, fmt.Errorf("ACME Storage: multiple records matched key %s", key) | ||||
| 	} | ||||
| 	b := bytes.NewBuffer(records[0].Value) | ||||
| 	d := gob.NewDecoder(b) | ||||
| 	var f file | ||||
| 	err = d.Decode(&f) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return f.contents, nil | ||||
| } | ||||
|  | ||||
| func (s *storage) Delete(key string) error { | ||||
| 	return s.store.Delete(key) | ||||
| } | ||||
|  | ||||
| func (s *storage) Exists(key string) bool { | ||||
| 	_, err := s.store.Read() | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func (s *storage) List(prefix string, recursive bool) ([]string, error) { | ||||
| 	records, err := s.store.Sync() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	var results []string | ||||
| 	for _, r := range records { | ||||
| 		if strings.HasPrefix(r.Key, prefix) { | ||||
| 			results = append(results, r.Key) | ||||
| 		} | ||||
| 	} | ||||
| 	if recursive { | ||||
| 		return results, nil | ||||
| 	} | ||||
| 	keysMap := make(map[string]bool) | ||||
| 	for _, key := range results { | ||||
| 		dir := strings.Split(strings.TrimPrefix(key, prefix+"/"), "/") | ||||
| 		keysMap[dir[0]] = true | ||||
| 	} | ||||
| 	results = make([]string, 0) | ||||
| 	for k := range keysMap { | ||||
| 		results = append(results, path.Join(prefix, k)) | ||||
| 	} | ||||
| 	return results, nil | ||||
| } | ||||
|  | ||||
| func (s *storage) Stat(key string) (certmagic.KeyInfo, error) { | ||||
| 	records, err := s.store.Read(key) | ||||
| 	if err != nil { | ||||
| 		return certmagic.KeyInfo{}, err | ||||
| 	} | ||||
| 	if len(records) != 1 { | ||||
| 		return certmagic.KeyInfo{}, fmt.Errorf("ACME Storage: multiple records matched key %s", key) | ||||
| 	} | ||||
| 	b := bytes.NewBuffer(records[0].Value) | ||||
| 	d := gob.NewDecoder(b) | ||||
| 	var f file | ||||
| 	err = d.Decode(&f) | ||||
| 	if err != nil { | ||||
| 		return certmagic.KeyInfo{}, err | ||||
| 	} | ||||
| 	return certmagic.KeyInfo{ | ||||
| 		Key:        key, | ||||
| 		Modified:   f.lastModified, | ||||
| 		Size:       int64(len(f.contents)), | ||||
| 		IsTerminal: false, | ||||
| 	}, nil | ||||
| } | ||||
							
								
								
									
										73
									
								
								api/server/acme/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								api/server/acme/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| package acme | ||||
|  | ||||
| import "github.com/go-acme/lego/v3/challenge" | ||||
|  | ||||
| // Option (or Options) are passed to New() to configure providers | ||||
| type Option func(o *Options) | ||||
|  | ||||
| // Options represents various options you can present to ACME providers | ||||
| type Options struct { | ||||
| 	// AcceptTLS must be set to true to indicate that you have read your | ||||
| 	// provider's terms of service. | ||||
| 	AcceptToS bool | ||||
| 	// CA is the CA to use | ||||
| 	CA string | ||||
| 	// ChallengeProvider is a go-acme/lego challenge provider. Set this if you | ||||
| 	// want to use DNS Challenges. Otherwise, tls-alpn-01 will be used | ||||
| 	ChallengeProvider challenge.Provider | ||||
| 	// Issue certificates for domains on demand. Otherwise, certs will be | ||||
| 	// retrieved / issued on start-up. | ||||
| 	OnDemand bool | ||||
| 	// Cache is a storage interface. Most ACME libraries have an cache, but | ||||
| 	// there's no defined interface, so if you consume this option | ||||
| 	// sanity check it before using. | ||||
| 	Cache interface{} | ||||
| } | ||||
|  | ||||
| // AcceptToS indicates whether you accept your CA's terms of service | ||||
| func AcceptToS(b bool) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.AcceptToS = b | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // CA sets the CA of an acme.Options | ||||
| func CA(CA string) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.CA = CA | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ChallengeProvider sets the Challenge provider of an acme.Options | ||||
| // if set, it enables the DNS challenge, otherwise tls-alpn-01 will be used. | ||||
| func ChallengeProvider(p challenge.Provider) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.ChallengeProvider = p | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // OnDemand enables on-demand certificate issuance. Not recommended for use | ||||
| // with the DNS challenge, as the first connection may be very slow. | ||||
| func OnDemand(b bool) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.OnDemand = b | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Cache provides a cache / storage interface to the underlying ACME library | ||||
| // as there is no standard, this needs to be validated by the underlying | ||||
| // implentation. | ||||
| func Cache(c interface{}) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.Cache = c | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Default uses the Let's Encrypt Production CA, with DNS Challenge disabled. | ||||
| func Default() []Option { | ||||
| 	return []Option{ | ||||
| 		AcceptToS(true), | ||||
| 		CA(LetsEncryptProductionCA), | ||||
| 		OnDemand(true), | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user