Implementation of certmagic storage using micro's store and sync packages
This commit is contained in:
		| @@ -2,7 +2,7 @@ language: go | |||||||
| go: | go: | ||||||
| - 1.13.x | - 1.13.x | ||||||
| env: | env: | ||||||
|   - GO111MODULE=on |   - GO111MODULE=on IN_TRAVIS_CI=yes | ||||||
| notifications: | notifications: | ||||||
|   slack: |   slack: | ||||||
|     secure: aEvhLbhujaGaKSrOokiG3//PaVHTIrc3fBpoRbCRqfZpyq6WREoapJJhF+tIpWWOwaC9GmChbD6aHo/jMUgwKXVyPSaNjiEL87YzUUpL8B2zslNp1rgfTg/LrzthOx3Q1TYwpaAl3to0fuHUVFX4yMeC2vuThq7WSXgMMxFCtbc= |     secure: aEvhLbhujaGaKSrOokiG3//PaVHTIrc3fBpoRbCRqfZpyq6WREoapJJhF+tIpWWOwaC9GmChbD6aHo/jMUgwKXVyPSaNjiEL87YzUUpL8B2zslNp1rgfTg/LrzthOx3Q1TYwpaAl3to0fuHUVFX4yMeC2vuThq7WSXgMMxFCtbc= | ||||||
|   | |||||||
| @@ -4,8 +4,6 @@ package acme | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"net" | 	"net" | ||||||
|  |  | ||||||
| 	"github.com/go-acme/lego/v3/challenge" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| @@ -24,62 +22,3 @@ const ( | |||||||
| 	LetsEncryptStagingCA    = "https://acme-staging-v02.api.letsencrypt.org/directory" | 	LetsEncryptStagingCA    = "https://acme-staging-v02.api.letsencrypt.org/directory" | ||||||
| 	LetsEncryptProductionCA = "https://acme-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 | package certmagic | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"log" | ||||||
| 	"net" | 	"net" | ||||||
|  |  | ||||||
| 	"github.com/mholt/certmagic" | 	"github.com/mholt/certmagic" | ||||||
| @@ -36,6 +37,12 @@ func New(options ...acme.Option) acme.Provider { | |||||||
| 			op(o) | 			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{ | 	return &certmagicProvider{ | ||||||
| 		opts: o, | 		opts: o, | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1,19 +1,24 @@ | |||||||
| package certmagic | package certmagic | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"os" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/go-acme/lego/v3/providers/dns/cloudflare" | 	"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) { | func TestCertMagic(t *testing.T) { | ||||||
| 	// TODO: Travis doesn't let us bind :443 | 	if len(os.Getenv("IN_TRAVIS_CI")) != 0 { | ||||||
| 	// l, err := New().NewListener() | 		t.Skip("Travis doesn't let us bind :443") | ||||||
| 	// if err != nil { | 	} | ||||||
| 	// 	t.Error(err.Error()) | 	l, err := New().NewListener() | ||||||
| 	// } | 	if err != nil { | ||||||
| 	// l.Close() | 		t.Error(err.Error()) | ||||||
|  | 	} | ||||||
|  | 	l.Close() | ||||||
|  |  | ||||||
| 	c := cloudflare.NewDefaultConfig() | 	c := cloudflare.NewDefaultConfig() | ||||||
| 	c.AuthEmail = "" | 	c.AuthEmail = "" | ||||||
| @@ -21,19 +26,30 @@ func TestCertMagic(t *testing.T) { | |||||||
| 	c.AuthToken = "test" | 	c.AuthToken = "test" | ||||||
| 	c.ZoneToken = "test" | 	c.ZoneToken = "test" | ||||||
|  |  | ||||||
| 	_, err := cloudflare.NewDNSProviderConfig(c) | 	p, err := cloudflare.NewDNSProviderConfig(c) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Error(err.Error()) | 		t.Error(err.Error()) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// TODO: Travis doesn't let us bind :443 | 	l, err = New(acme.AcceptToS(true), | ||||||
| 	// l, err = New(acme.AcceptTLS(true), | 		acme.CA(acme.LetsEncryptStagingCA), | ||||||
| 	// 	acme.CA(acme.LetsEncryptStagingCA), | 		acme.ChallengeProvider(p), | ||||||
| 	// 	acme.ChallengeProvider(p), | 	).NewListener() | ||||||
| 	// ).NewListener() |  | ||||||
|  |  | ||||||
| 	// if err != nil { | 	if err != nil { | ||||||
| 	// 	t.Error(err.Error()) | 		t.Error(err.Error()) | ||||||
| 	// } | 	} | ||||||
| 	// l.Close() | 	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