From a4e048d79833faabf4e883114dc72360ae1c97ad Mon Sep 17 00:00:00 2001 From: Craig Peterson Date: Tue, 24 Jul 2018 13:26:21 -0400 Subject: [PATCH 1/5] implement dynamic provider to delegate to other providers dynamically --- providers/dns/multi/config.go | 57 ++++++++++ providers/dns/multi/multi.go | 198 ++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 providers/dns/multi/config.go create mode 100644 providers/dns/multi/multi.go diff --git a/providers/dns/multi/config.go b/providers/dns/multi/config.go new file mode 100644 index 0000000000..55a236f5e2 --- /dev/null +++ b/providers/dns/multi/config.go @@ -0,0 +1,57 @@ +package multi + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strings" +) + +// MultiProviderConfig is the configuration for a multiple provider setup. This is expected to be given in json format via +// MULTI_CONFIG environment variable, or in a file location specified by MULTI_CONFIG_FILE. +type MultiProviderConfig struct { + // Domain names to list of provider names + Domains map[string][]string + // Provider Name -> Key/Value pairs for environment + Providers map[string]map[string]string +} + +// providerNamesForDomain chooses the most appropriate domain from the config and returns its' list of dns providers +// looks for most specific match to least specific, one dot at a time. Finally folling back to "default" domain. +func (m *MultiProviderConfig) providerNamesForDomain(domain string) ([]string, error) { + parts := strings.Split(domain, ".") + var names []string + for i := 0; i < len(parts); i++ { + partial := strings.Join(parts[i:], ".") + if names = m.Domains[partial]; names != nil { + break + } + } + if names == nil { + names = m.Domains["default"] + } + if names == nil { + return nil, fmt.Errorf("Couldn't find any suitable dns provider for domain %s", domain) + } + return names, nil +} + +func getConfig() (*MultiProviderConfig, error) { + var rawJSON []byte + var err error + if cfg := os.Getenv("MULTI_CONFIG"); cfg != "" { + rawJSON = []byte(cfg) + } else if path := os.Getenv("MULTI_CONFIG_FILE"); path != "" { + if rawJSON, err = ioutil.ReadFile(path); err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("'multi' provider requires json config in MULTI_CONFIG or MULTI_CONFIG_PATH") + } + cfg := &MultiProviderConfig{} + if err = json.Unmarshal(rawJSON, cfg); err != nil { + return nil, err + } + return cfg, nil +} diff --git a/providers/dns/multi/multi.go b/providers/dns/multi/multi.go new file mode 100644 index 0000000000..db8bd8bb9d --- /dev/null +++ b/providers/dns/multi/multi.go @@ -0,0 +1,198 @@ +// package multi implements a dynamic challenge provider that can select different dns providers for different domains, +// and even multiple distinct dns providers and accounts for each individual domain. This can be useful if: +// +// - Multiple dns providers are used for active-active redundant dns service +// +// - You need a single certificate issued for different domains, each using different dns services +// +// Configuration is given by selecting DNS provider type "multi", and by giving further per-domain information via a json object: +// +// { +// "Providers": { +// "cloudflare": { +// "CLOUDFLARE_EMAIL": "myacct@example.com", +// "CLOUDFLARE_API_KEY": "123qwerty" +// }, +// "digitalocean":{ +// "DO_AUTH_TOKEN": "456uiop" +// } +// } +// "Domains": { +// "example.com": ["digitalocean"], +// "example.org": ["cloudflare"], +// "example.net": ["digitalocean, cloudflare"] +// } +// } +// +// In the above json, each "Provider" is a named provider instance along with the associated credentials. The credentials will be set as environment +// variables as appropriate when the provider is instantiated for the first time. +// +// If the provider name is the same as a registered provider type (like "cloudflare"), the type will be inferred. If it is not the same (perhaps in cases where multiple +// different accounts are involved), you may specify it with the `type` field on the provider object. +// +// Domains are then linked to one or more of the named providers by name. Challenges will be filled on every provider specified for the domain. When looking for a domain +// configuration, config domains will be checked from most specific to least specific by each dot. For example, to fill a challenge for `foo.example.com`, +// a configured domain for `foo.example.com` will be looked for, failing that it will look for `example.com` and `com` in that order. If there is still no match and a +// domain with the name `default` is found, that will be used. Otherwise an error will be returned. +// +// The json configuration for domains can be specified directly via environment variable (`MULTI_CONFIG`), or from a file referenced by `MULTI_CONFIG_FILE`. +package multi + +import ( + "fmt" + "os" + "time" + + "github.com/xenolf/lego/acme" +) + +// NewDNSChallengeProviderByName is defined here to avoid recursive imports, this must be injected by the dns package so that +// the delegated dns providers may be dynamically instantiated +var NewDNSChallengeProviderByName func(string) (acme.ChallengeProvider, error) + +type MultiProvider struct { + config *MultiProviderConfig + providers map[string]acme.ChallengeProvider +} + +// AggregateProvider is simply a list of dns providers. All Challenges are filled by all members of the aggregate. +type AggregateProvider []acme.ChallengeProvider + +func (a AggregateProvider) Present(domain, token, keyAuth string) error { + for _, p := range a { + if err := p.Present(domain, token, keyAuth); err != nil { + return err + } + } + return nil +} +func (a AggregateProvider) CleanUp(domain, token, keyAuth string) error { + for _, p := range a { + if err := p.CleanUp(domain, token, keyAuth); err != nil { + return err + } + } + return nil +} + +// AggregateProviderTimeout is simply a list of dns providers. This type will be chosen when any of the 'subproviders' implement Timeout control. +// All Challenges are filled by all members of the aggregate. +// Timeout returned will be the maximum time of any child provider. +type AggregateProviderTimeout struct { + AggregateProvider +} + +func (a AggregateProviderTimeout) Timeout() (timeout, interval time.Duration) { + for _, p := range a.AggregateProvider { + if to, ok := p.(acme.ChallengeProviderTimeout); ok { + t, i := to.Timeout() + if t > timeout { + timeout = t + } + if i > interval { + interval = i + } + } + } + return +} + +func (m *MultiProvider) getProviderForDomain(domain string) (acme.ChallengeProvider, error) { + names, err := m.config.providerNamesForDomain(domain) + if err != nil { + return nil, err + } + var agg AggregateProvider + anyTimeouts := false + for _, n := range names { + p, err := m.providerByName(n) + if err != nil { + return nil, err + } + if _, ok := p.(acme.ChallengeProviderTimeout); ok { + anyTimeouts = true + } + agg = append(agg, p) + } + // don't wrap provider in aggregate if there is only one + if len(agg) == 1 { + return agg[0], nil + } + if anyTimeouts { + return AggregateProviderTimeout{agg}, nil + } + return agg, nil +} + +func (m *MultiProvider) providerByName(name string) (acme.ChallengeProvider, error) { + if p, ok := m.providers[name]; ok { + return p, nil + } + if params, ok := m.config.Providers[name]; ok { + return m.buildProvider(name, params) + } + return nil, fmt.Errorf("Couldn't find appropriate config for dns provider named '%s'", name) +} + +func (m *MultiProvider) buildProvider(name string, params map[string]string) (acme.ChallengeProvider, error) { + pType := name + origEnv := map[string]string{} + + // copy parameters into environment, keeping track of previous values + for k, v := range params { + if k == "type" { + pType = v + continue + } + if oldVal, ok := os.LookupEnv(k); ok { + origEnv[k] = oldVal + } + os.Setenv(k, v) + } + // restore previous values + defer func() { + for k := range params { + if k == "type" { + continue + } + if oldVal, ok := origEnv[k]; ok { + os.Setenv(k, oldVal) + } else { + os.Unsetenv(k) + } + } + }() + prv, err := NewDNSChallengeProviderByName(pType) + if err != nil { + return nil, err + } + m.providers[name] = prv + return prv, nil +} + +func New() (*MultiProvider, error) { + config, err := getConfig() + if err != nil { + return nil, err + } + return &MultiProvider{ + providers: map[string]acme.ChallengeProvider{}, + config: config, + }, nil +} + +func (m *MultiProvider) Present(domain, token, keyAuth string) error { + provider, err := m.getProviderForDomain(domain) + if err != nil { + return err + } + return provider.Present(domain, token, keyAuth) +} + +func (m *MultiProvider) CleanUp(domain, token, keyAuth string) error { + provider, err := m.getProviderForDomain(domain) + if err != nil { + return err + } + return provider.CleanUp(domain, token, keyAuth) +} From c2359c2e412e2552e84e53c4e4301a03a65334dc Mon Sep 17 00:00:00 2001 From: Craig Peterson Date: Tue, 24 Jul 2018 15:49:03 -0400 Subject: [PATCH 2/5] inject the correct method to the multi package --- providers/dns/dns_providers.go | 7 +++++++ providers/dns/multi/config.go | 2 +- providers/dns/multi/multi.go | 6 +++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 604137d566..69dc5e15ef 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -34,6 +34,7 @@ import ( "github.com/xenolf/lego/providers/dns/lightsail" "github.com/xenolf/lego/providers/dns/linode" "github.com/xenolf/lego/providers/dns/linodev4" + "github.com/xenolf/lego/providers/dns/multi" "github.com/xenolf/lego/providers/dns/mydnsjp" "github.com/xenolf/lego/providers/dns/namecheap" "github.com/xenolf/lego/providers/dns/namedotcom" @@ -120,6 +121,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) return linodev4.NewDNSProvider() case "manual": return acme.NewDNSProviderManual() + case "multi": + return multi.New() case "mydnsjp": return mydnsjp.NewDNSProvider() case "namecheap": @@ -162,3 +165,7 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) return nil, fmt.Errorf("unrecognised DNS provider: %s", name) } } + +func init() { + multi.NewDNSChallengeProviderByName = NewDNSChallengeProviderByName +} diff --git a/providers/dns/multi/config.go b/providers/dns/multi/config.go index 55a236f5e2..4fb5a4394a 100644 --- a/providers/dns/multi/config.go +++ b/providers/dns/multi/config.go @@ -47,7 +47,7 @@ func getConfig() (*MultiProviderConfig, error) { return nil, err } } else { - return nil, fmt.Errorf("'multi' provider requires json config in MULTI_CONFIG or MULTI_CONFIG_PATH") + return nil, fmt.Errorf("'multi' provider requires json config in MULTI_CONFIG or MULTI_CONFIG_FILE") } cfg := &MultiProviderConfig{} if err = json.Unmarshal(rawJSON, cfg); err != nil { diff --git a/providers/dns/multi/multi.go b/providers/dns/multi/multi.go index db8bd8bb9d..3b95e5af04 100644 --- a/providers/dns/multi/multi.go +++ b/providers/dns/multi/multi.go @@ -1,4 +1,4 @@ -// package multi implements a dynamic challenge provider that can select different dns providers for different domains, +// Package multi implements a dynamic challenge provider that can select different dns providers for different domains, // and even multiple distinct dns providers and accounts for each individual domain. This can be useful if: // // - Multiple dns providers are used for active-active redundant dns service @@ -11,10 +11,10 @@ // "Providers": { // "cloudflare": { // "CLOUDFLARE_EMAIL": "myacct@example.com", -// "CLOUDFLARE_API_KEY": "123qwerty" +// "CLOUDFLARE_API_KEY": "123qwerty..." // }, // "digitalocean":{ -// "DO_AUTH_TOKEN": "456uiop" +// "DO_AUTH_TOKEN": "456uiop..." // } // } // "Domains": { From 62bedaca6274437ad74e327767b2acf83cde0953 Mon Sep 17 00:00:00 2001 From: Craig Peterson Date: Tue, 24 Jul 2018 15:51:27 -0400 Subject: [PATCH 3/5] rename for consistency --- providers/dns/multi/multi.go | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/providers/dns/multi/multi.go b/providers/dns/multi/multi.go index 3b95e5af04..d8fae3cc2d 100644 --- a/providers/dns/multi/multi.go +++ b/providers/dns/multi/multi.go @@ -50,7 +50,8 @@ import ( // the delegated dns providers may be dynamically instantiated var NewDNSChallengeProviderByName func(string) (acme.ChallengeProvider, error) -type MultiProvider struct { +// DNSProvider implements a dns provider that selects which other providers to use for each domain individually. +type DNSProvider struct { config *MultiProviderConfig providers map[string]acme.ChallengeProvider } @@ -97,15 +98,15 @@ func (a AggregateProviderTimeout) Timeout() (timeout, interval time.Duration) { return } -func (m *MultiProvider) getProviderForDomain(domain string) (acme.ChallengeProvider, error) { - names, err := m.config.providerNamesForDomain(domain) +func (d *DNSProvider) getProviderForDomain(domain string) (acme.ChallengeProvider, error) { + names, err := d.config.providerNamesForDomain(domain) if err != nil { return nil, err } var agg AggregateProvider anyTimeouts := false for _, n := range names { - p, err := m.providerByName(n) + p, err := d.providerByName(n) if err != nil { return nil, err } @@ -124,17 +125,17 @@ func (m *MultiProvider) getProviderForDomain(domain string) (acme.ChallengeProvi return agg, nil } -func (m *MultiProvider) providerByName(name string) (acme.ChallengeProvider, error) { - if p, ok := m.providers[name]; ok { +func (d *DNSProvider) providerByName(name string) (acme.ChallengeProvider, error) { + if p, ok := d.providers[name]; ok { return p, nil } - if params, ok := m.config.Providers[name]; ok { - return m.buildProvider(name, params) + if params, ok := d.config.Providers[name]; ok { + return d.buildProvider(name, params) } return nil, fmt.Errorf("Couldn't find appropriate config for dns provider named '%s'", name) } -func (m *MultiProvider) buildProvider(name string, params map[string]string) (acme.ChallengeProvider, error) { +func (d *DNSProvider) buildProvider(name string, params map[string]string) (acme.ChallengeProvider, error) { pType := name origEnv := map[string]string{} @@ -166,31 +167,31 @@ func (m *MultiProvider) buildProvider(name string, params map[string]string) (ac if err != nil { return nil, err } - m.providers[name] = prv + d.providers[name] = prv return prv, nil } -func New() (*MultiProvider, error) { +func New() (*DNSProvider, error) { config, err := getConfig() if err != nil { return nil, err } - return &MultiProvider{ + return &DNSProvider{ providers: map[string]acme.ChallengeProvider{}, config: config, }, nil } -func (m *MultiProvider) Present(domain, token, keyAuth string) error { - provider, err := m.getProviderForDomain(domain) +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + provider, err := d.getProviderForDomain(domain) if err != nil { return err } return provider.Present(domain, token, keyAuth) } -func (m *MultiProvider) CleanUp(domain, token, keyAuth string) error { - provider, err := m.getProviderForDomain(domain) +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + provider, err := d.getProviderForDomain(domain) if err != nil { return err } From abba48031d6be794eb1fd6dbe2c3b0764e6a7628 Mon Sep 17 00:00:00 2001 From: Craig Peterson Date: Tue, 24 Jul 2018 16:03:16 -0400 Subject: [PATCH 4/5] lint --- providers/dns/multi/config.go | 10 +++++----- providers/dns/multi/multi.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/providers/dns/multi/config.go b/providers/dns/multi/config.go index 4fb5a4394a..a17f9a7c2e 100644 --- a/providers/dns/multi/config.go +++ b/providers/dns/multi/config.go @@ -8,9 +8,9 @@ import ( "strings" ) -// MultiProviderConfig is the configuration for a multiple provider setup. This is expected to be given in json format via +// ProviderConfig is the configuration for a multiple provider setup. This is expected to be given in json format via // MULTI_CONFIG environment variable, or in a file location specified by MULTI_CONFIG_FILE. -type MultiProviderConfig struct { +type ProviderConfig struct { // Domain names to list of provider names Domains map[string][]string // Provider Name -> Key/Value pairs for environment @@ -19,7 +19,7 @@ type MultiProviderConfig struct { // providerNamesForDomain chooses the most appropriate domain from the config and returns its' list of dns providers // looks for most specific match to least specific, one dot at a time. Finally folling back to "default" domain. -func (m *MultiProviderConfig) providerNamesForDomain(domain string) ([]string, error) { +func (m *ProviderConfig) providerNamesForDomain(domain string) ([]string, error) { parts := strings.Split(domain, ".") var names []string for i := 0; i < len(parts); i++ { @@ -37,7 +37,7 @@ func (m *MultiProviderConfig) providerNamesForDomain(domain string) ([]string, e return names, nil } -func getConfig() (*MultiProviderConfig, error) { +func getConfig() (*ProviderConfig, error) { var rawJSON []byte var err error if cfg := os.Getenv("MULTI_CONFIG"); cfg != "" { @@ -49,7 +49,7 @@ func getConfig() (*MultiProviderConfig, error) { } else { return nil, fmt.Errorf("'multi' provider requires json config in MULTI_CONFIG or MULTI_CONFIG_FILE") } - cfg := &MultiProviderConfig{} + cfg := &ProviderConfig{} if err = json.Unmarshal(rawJSON, cfg); err != nil { return nil, err } diff --git a/providers/dns/multi/multi.go b/providers/dns/multi/multi.go index d8fae3cc2d..fcf980a806 100644 --- a/providers/dns/multi/multi.go +++ b/providers/dns/multi/multi.go @@ -52,7 +52,7 @@ var NewDNSChallengeProviderByName func(string) (acme.ChallengeProvider, error) // DNSProvider implements a dns provider that selects which other providers to use for each domain individually. type DNSProvider struct { - config *MultiProviderConfig + config *ProviderConfig providers map[string]acme.ChallengeProvider } From 8612a4ee89045cc479c2fcc6828107123781d27b Mon Sep 17 00:00:00 2001 From: Craig Peterson Date: Tue, 24 Jul 2018 16:13:18 -0400 Subject: [PATCH 5/5] lint --- providers/dns/dns_providers.go | 2 +- providers/dns/multi/multi.go | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 69dc5e15ef..d5ec62ff3b 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -122,7 +122,7 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) case "manual": return acme.NewDNSProviderManual() case "multi": - return multi.New() + return multi.NewDNSProvider() case "mydnsjp": return mydnsjp.NewDNSProvider() case "namecheap": diff --git a/providers/dns/multi/multi.go b/providers/dns/multi/multi.go index fcf980a806..88ea49a090 100644 --- a/providers/dns/multi/multi.go +++ b/providers/dns/multi/multi.go @@ -56,9 +56,22 @@ type DNSProvider struct { providers map[string]acme.ChallengeProvider } +// NewDNSProvider creates a new multiple-provider meta-provider. It will look for a json configuration in "MULTI_CONFIG", or on disk from "MULTI_CONFIG_FILE" +func NewDNSProvider() (*DNSProvider, error) { + config, err := getConfig() + if err != nil { + return nil, err + } + return &DNSProvider{ + providers: map[string]acme.ChallengeProvider{}, + config: config, + }, nil +} + // AggregateProvider is simply a list of dns providers. All Challenges are filled by all members of the aggregate. type AggregateProvider []acme.ChallengeProvider +// Present creates the txt record in all child dns providers func (a AggregateProvider) Present(domain, token, keyAuth string) error { for _, p := range a { if err := p.Present(domain, token, keyAuth); err != nil { @@ -67,6 +80,8 @@ func (a AggregateProvider) Present(domain, token, keyAuth string) error { } return nil } + +// CleanUp removes the txt record from all dns providers func (a AggregateProvider) CleanUp(domain, token, keyAuth string) error { for _, p := range a { if err := p.CleanUp(domain, token, keyAuth); err != nil { @@ -83,6 +98,7 @@ type AggregateProviderTimeout struct { AggregateProvider } +// Timeout gives the largest timeout values from any child provider that supports timeouts. func (a AggregateProviderTimeout) Timeout() (timeout, interval time.Duration) { for _, p := range a.AggregateProvider { if to, ok := p.(acme.ChallengeProviderTimeout); ok { @@ -171,17 +187,6 @@ func (d *DNSProvider) buildProvider(name string, params map[string]string) (acme return prv, nil } -func New() (*DNSProvider, error) { - config, err := getConfig() - if err != nil { - return nil, err - } - return &DNSProvider{ - providers: map[string]acme.ChallengeProvider{}, - config: config, - }, nil -} - func (d *DNSProvider) Present(domain, token, keyAuth string) error { provider, err := d.getProviderForDomain(domain) if err != nil {