Implementation of certmagic storage using micro's store and sync packages
This commit is contained in:
parent
b1d5dc20fa
commit
a6e95d389f
@ -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),
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user