From 8e4266106034819fa0f4be8f0efbd628eb3e1634 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sat, 2 May 2020 17:23:36 -0600 Subject: caddytls: Finish upgrading to libdns DNS providers for ACME challenges Until we finish the migration to the new acme library, we have to bring the solver type in-house. It's small and temporary. --- modules/caddytls/acmeissuer.go | 16 +++++- modules/caddytls/dnssolver.go | 113 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 modules/caddytls/dnssolver.go (limited to 'modules/caddytls') diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index dcdfc13..4295cda 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -91,7 +91,21 @@ func (m *ACMEIssuer) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("loading DNS provider module: %v", err) } - m.Challenges.DNS.provider = val.(challenge.Provider) + // TODO: For a temporary amount of time, we are allowing the use of + // DNS providers from go-acme/lego since there are so many implemented + // for it -- they are adapted as Caddy modules in this repository: + // https://github.com/caddy-dns/lego-deprecated - that module is + // a challenge.Provider value, so we use it directly. The user must set + // environment variables to configure it. Remove this shim once a sufficient + // number of DNS providers are implemented for the libdns APIs instead. + if grandfatheredProvider, ok := val.(challenge.Provider); ok { + m.Challenges.DNS.provider = grandfatheredProvider + } else { + m.Challenges.DNS.provider = &solver{ + recordManager: val.(recordManager), + TTL: time.Duration(m.Challenges.DNS.TTL), + } + } } // add any custom CAs to trust store diff --git a/modules/caddytls/dnssolver.go b/modules/caddytls/dnssolver.go new file mode 100644 index 0000000..c8a9c3a --- /dev/null +++ b/modules/caddytls/dnssolver.go @@ -0,0 +1,113 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddytls + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/go-acme/lego/v3/challenge" + "github.com/go-acme/lego/v3/challenge/dns01" + "github.com/libdns/libdns" +) + +// TODO: this is borrowed from https://github.com/mholt/acme - once we +// switch to that acme library, this file will go away + +// solver is a type that makes libdns providers usable as ACME challenge solvers. +type solver struct { + recordManager + + TTL time.Duration + + txtRecords map[string]libdns.Record // keyed by challenge token + txtRecordsMu sync.Mutex +} + +func (s *solver) Present(domain, token, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + + rec := libdns.Record{ + Type: "TXT", + Name: fqdn, + Value: value, + TTL: s.TTL, + } + + zone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return fmt.Errorf("could not determine zone for domain %q: %v", fqdn, err) + } + + results, err := s.recordManager.AppendRecords(context.TODO(), zone, []libdns.Record{rec}) + if err != nil { + return err + } + if len(results) != 1 { + return fmt.Errorf("expected one record, got %d: %v", len(results), results) + } + + // keep this record handy so we can clean it up more efficiently + s.txtRecordsMu.Lock() + if s.txtRecords == nil { + s.txtRecords = make(map[string]libdns.Record) + } + s.txtRecords[keyAuth] = results[0] + s.txtRecordsMu.Unlock() + + // TODO: check for record propagation before continuing (accordig to config) + + return nil +} + +func (s *solver) CleanUp(domain, token, keyAuth string) error { + fqdn, _ := dns01.GetRecord(domain, keyAuth) + authZone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return err + } + + // retrieve the record we created + s.txtRecordsMu.Lock() + txtRec, ok := s.txtRecords[keyAuth] + if !ok { + s.txtRecordsMu.Unlock() + return fmt.Errorf("no memory of presenting a DNS record for %v", domain) + } + s.txtRecordsMu.Unlock() + + // clean up the record + _, err = s.recordManager.DeleteRecords(context.TODO(), authZone, []libdns.Record{txtRec}) + if err != nil { + return err + } + + // once it has been successfully cleaned up, we can forget about it + s.txtRecordsMu.Lock() + delete(s.txtRecords, keyAuth) + s.txtRecordsMu.Unlock() + + return nil +} + +// recordManager defines the set of operations required for ACME challenges. +type recordManager interface { + libdns.RecordAppender + libdns.RecordDeleter +} + +var _ challenge.Provider = (*solver)(nil) -- cgit v1.2.3