diff --git a/api/server/acme/certmagic/certmagic.go b/api/server/acme/certmagic/certmagic.go index 6d12e9d6..cde5783d 100644 --- a/api/server/acme/certmagic/certmagic.go +++ b/api/server/acme/certmagic/certmagic.go @@ -3,7 +3,9 @@ package certmagic import ( "log" + "math/rand" "net" + "time" "github.com/mholt/certmagic" @@ -15,6 +17,7 @@ type certmagicProvider struct { } func (c *certmagicProvider) NewListener(ACMEHosts ...string) (net.Listener, error) { + certmagic.Default.CA = c.opts.CA if c.opts.ChallengeProvider != nil { // Enabling DNS Challenge disables the other challenges certmagic.Default.DNSProvider = c.opts.ChallengeProvider @@ -22,6 +25,16 @@ func (c *certmagicProvider) NewListener(ACMEHosts ...string) (net.Listener, erro if c.opts.OnDemand { certmagic.Default.OnDemand = new(certmagic.OnDemandConfig) } + if c.opts.Cache != nil { + // already validated by new() + certmagic.Default.Storage = c.opts.Cache.(certmagic.Storage) + } + // If multiple instances of the provider are running, inject some + // randomness so they don't collide + rand.Seed(time.Now().UnixNano()) + randomDuration := (7 * 24 * time.Hour) + (time.Duration(rand.Intn(504)) * time.Hour) + certmagic.Default.RenewDurationBefore = randomDuration + return certmagic.Listen(ACMEHosts) } diff --git a/api/server/acme/certmagic/certmagic_test.go b/api/server/acme/certmagic/certmagic_test.go index 367f6360..8474b0bd 100644 --- a/api/server/acme/certmagic/certmagic_test.go +++ b/api/server/acme/certmagic/certmagic_test.go @@ -1,6 +1,7 @@ package certmagic import ( + "net/http" "os" "reflect" "sort" @@ -185,3 +186,46 @@ func TestStorageImplementation(t *testing.T) { // happens New(acme.Cache(s)) } + +// Full test with a real zone, with against LE staging +func TestE2e(t *testing.T) { + apiToken, accountID := os.Getenv("CF_API_TOKEN"), os.Getenv("CF_ACCOUNT_ID") + kvID := os.Getenv("KV_NAMESPACE_ID") + if len(apiToken) == 0 || len(accountID) == 0 || len(kvID) == 0 { + t.Skip("No Cloudflare API keys available, skipping test") + } + + testLock := memory.NewLock() + testStore, err := cloudflarestorage.New( + options.WithValue("CF_API_TOKEN", apiToken), + options.WithValue("CF_ACCOUNT_ID", accountID), + options.WithValue("KV_NAMESPACE_ID", kvID), + ) + if err != nil { + t.Fatal(err.Error()) + } + testStorage := NewStorage(testLock, testStore) + + conf := cloudflare.NewDefaultConfig() + conf.AuthToken = apiToken + conf.ZoneToken = apiToken + testChallengeProvider, err := cloudflare.NewDNSProviderConfig(conf) + if err != nil { + t.Fatal(err.Error()) + } + + testProvider := New( + acme.AcceptToS(true), + acme.Cache(testStorage), + acme.CA(acme.LetsEncryptStagingCA), + acme.ChallengeProvider(testChallengeProvider), + acme.OnDemand(false), + ) + + listener, err := testProvider.NewListener("*.micro.mu", "micro.mu") + if err != nil { + t.Fatal(err.Error()) + } + go http.Serve(listener, http.NotFoundHandler()) + time.Sleep(10 * time.Minute) +} diff --git a/api/server/acme/certmagic/storage.go b/api/server/acme/certmagic/storage.go index d0dd3ab9..b91b5db6 100644 --- a/api/server/acme/certmagic/storage.go +++ b/api/server/acme/certmagic/storage.go @@ -3,6 +3,7 @@ package certmagic import ( "bytes" "encoding/gob" + "errors" "fmt" "path" "strings" @@ -55,6 +56,9 @@ func (s *storage) Store(key string, value []byte) error { } func (s *storage) Load(key string) ([]byte, error) { + if !s.Exists(key) { + return nil, certmagic.ErrNotExist(errors.New(key + " doesn't exist")) + } records, err := s.store.Read(key) if err != nil { return nil, err @@ -77,7 +81,7 @@ func (s *storage) Delete(key string) error { } func (s *storage) Exists(key string) bool { - _, err := s.store.Read() + _, err := s.store.Read(key) if err != nil { return false } @@ -132,3 +136,11 @@ func (s *storage) Stat(key string) (certmagic.KeyInfo, error) { IsTerminal: false, }, nil } + +// NewStorage returns a certmagic.Storage backed by a go-micro/lock and go-micro/store +func NewStorage(lock lock.Lock, store store.Store) certmagic.Storage { + return &storage{ + lock: lock, + store: store, + } +}