From 0ce6ba36b95542f1ef1beebb416a03130924837f Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 21 Mar 2019 11:52:01 +0100 Subject: [PATCH] Add DNS Provider for Domain Offensive (do.de) (#835) --- README.md | 32 ++++----- cmd/zz_gen_cmd_dnshelp.go | 21 ++++++ docs/content/dns/zz_gen_dode.md | 59 ++++++++++++++++ providers/dns/dns_providers.go | 3 + providers/dns/dode/client.go | 57 ++++++++++++++++ providers/dns/dode/dode.go | 89 ++++++++++++++++++++++++ providers/dns/dode/dode.toml | 19 ++++++ providers/dns/dode/dode_test.go | 115 ++++++++++++++++++++++++++++++++ 8 files changed, 379 insertions(+), 16 deletions(-) create mode 100644 docs/content/dns/zz_gen_dode.md create mode 100644 providers/dns/dode/client.go create mode 100644 providers/dns/dode/dode.go create mode 100644 providers/dns/dode/dode.toml create mode 100644 providers/dns/dode/dode_test.go diff --git a/README.md b/README.md index fe1c58b4..353b886d 100644 --- a/README.md +++ b/README.md @@ -41,19 +41,19 @@ Documentation is hosted live at https://go-acme.github.io/lego/. Detailed documentation is available [here](https://go-acme.github.io/lego/dns). -| | | | | -|----------------------------------------------------------------|--------------------------------------------------------------------------------|-------------------------------------------------------------------|------------------------------------------------------------------| -| [Alibaba Cloud DNS](https://go-acme.github.io/lego/dns/alidns/) | [Amazon Lightsail](https://go-acme.github.io/lego/dns/lightsail/) | [Amazon Route 53](https://go-acme.github.io/lego/dns/route53/) | [Aurora DNS](https://go-acme.github.io/lego/dns/auroradns/) | -| [Azure](https://go-acme.github.io/lego/dns/azure/) | [Bluecat](https://go-acme.github.io/lego/dns/bluecat/) | [ClouDNS](https://go-acme.github.io/lego/dns/cloudns/) | [CloudXNS](https://go-acme.github.io/lego/dns/cloudxns/) | -| [Cloudflare](https://go-acme.github.io/lego/dns/cloudflare/) | [ConoHa](https://go-acme.github.io/lego/dns/conoha/) | [DNS Made Easy](https://go-acme.github.io/lego/dns/dnsmadeeasy/) | [DNSPod](https://go-acme.github.io/lego/dns/dnspod/) | -| [DNSimple](https://go-acme.github.io/lego/dns/dnsimple/) | [Designate DNSaaS for Openstack](https://go-acme.github.io/lego/dns/designate/) | [Digital Ocean](https://go-acme.github.io/lego/dns/digitalocean/) | [DreamHost](https://go-acme.github.io/lego/dns/dreamhost/) | -| [Duck DNS](https://go-acme.github.io/lego/dns/duckdns/) | [Dyn](https://go-acme.github.io/lego/dns/dyn/) | [Exoscale](https://go-acme.github.io/lego/dns/exoscale/) | [External program](https://go-acme.github.io/lego/dns/exec/) | -| [FastDNS](https://go-acme.github.io/lego/dns/fastdns/) | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) | -| [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) | [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) | -| [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns) | [Linode (deprecated)](https://go-acme.github.io/lego/dns/linode/) | -| [Linode (v4)](https://go-acme.github.io/lego/dns/linodev4/) | [Manual](https://go-acme.github.io/lego/dns/manual/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | -| [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | -| [OVH](https://go-acme.github.io/lego/dns/ovh/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | -| [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | -| [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | -| [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | | | +| | | | | +|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------| +| [Alibaba Cloud DNS](https://go-acme.github.io/lego/dns/alidns/) | [Amazon Lightsail](https://go-acme.github.io/lego/dns/lightsail/) | [Amazon Route 53](https://go-acme.github.io/lego/dns/route53/) | [Aurora DNS](https://go-acme.github.io/lego/dns/auroradns/) | +| [Azure](https://go-acme.github.io/lego/dns/azure/) | [Bluecat](https://go-acme.github.io/lego/dns/bluecat/) | [Cloudflare](https://go-acme.github.io/lego/dns/cloudflare/) | [ClouDNS](https://go-acme.github.io/lego/dns/cloudns/) | +| [CloudXNS](https://go-acme.github.io/lego/dns/cloudxns/) | [ConoHa](https://go-acme.github.io/lego/dns/conoha/) | [Designate DNSaaS for Openstack](https://go-acme.github.io/lego/dns/designate/) | [Digital Ocean](https://go-acme.github.io/lego/dns/digitalocean/) | +| [DNS Made Easy](https://go-acme.github.io/lego/dns/dnsmadeeasy/) | [DNSimple](https://go-acme.github.io/lego/dns/dnsimple/) | [DNSPod](https://go-acme.github.io/lego/dns/dnspod/) | [Domain Offensive (do.de)](https://go-acme.github.io/lego/dns/dode/) | +| [DreamHost](https://go-acme.github.io/lego/dns/dreamhost/) | [Duck DNS](https://go-acme.github.io/lego/dns/duckdns/) | [Dyn](https://go-acme.github.io/lego/dns/dyn/) | [Exoscale](https://go-acme.github.io/lego/dns/exoscale/) | +| [External program](https://go-acme.github.io/lego/dns/exec/) | [FastDNS](https://go-acme.github.io/lego/dns/fastdns/) | [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | +| [Glesys](https://go-acme.github.io/lego/dns/glesys/) | [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) | [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) | +| [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns) | +| [Linode (deprecated)](https://go-acme.github.io/lego/dns/linode/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linodev4/) | [Manual](https://go-acme.github.io/lego/dns/manual/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | +| [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | +| [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) | +| [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | +| [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | +| [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | | diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index a5365849..5bdefabe 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -30,6 +30,7 @@ func allDNSCodes() string { "dnsimple", "dnsmadeeasy", "dnspod", + "dode", "dreamhost", "duckdns", "dyn", @@ -361,6 +362,26 @@ func displayDNSHelp(name string) { fmt.Fprintln(w) fmt.Fprintln(w, `More information: https://go-acme.github.io/lego/dns/dnspod`) + case "dode": + // generated from: providers/dns/dode/dode.toml + fmt.Fprintln(w, `Configuration for Domain Offensive (do.de).`) + fmt.Fprintln(w, `Code: 'dode'`) + fmt.Fprintln(w) + + fmt.Fprintln(w, `Credentials:`) + fmt.Fprintln(w, ` - "DODE_TOKEN": API token`) + fmt.Fprintln(w) + + fmt.Fprintln(w, `Additional Configuration:`) + fmt.Fprintln(w, ` - "DODE_HTTP_TIMEOUT": API request timeout`) + fmt.Fprintln(w, ` - "DODE_POLLING_INTERVAL": Time between DNS propagation check`) + fmt.Fprintln(w, ` - "DODE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + fmt.Fprintln(w, ` - "DODE_SEQUENCE_INTERVAL": Interval between iteration`) + fmt.Fprintln(w, ` - "DODE_TTL": The TTL of the TXT record used for the DNS challenge`) + + fmt.Fprintln(w) + fmt.Fprintln(w, `More information: https://go-acme.github.io/lego/dns/dode`) + case "dreamhost": // generated from: providers/dns/dreamhost/dreamhost.toml fmt.Fprintln(w, `Configuration for DreamHost.`) diff --git a/docs/content/dns/zz_gen_dode.md b/docs/content/dns/zz_gen_dode.md new file mode 100644 index 00000000..0ad6533e --- /dev/null +++ b/docs/content/dns/zz_gen_dode.md @@ -0,0 +1,59 @@ +--- +title: "Domain Offensive (do.de)" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: dode +--- + + + + + + +Configuration for [Domain Offensive (do.de)](https://www.do.de/). + + + + +- Code: `dode` + +{{% notice note %}} +_Please contribute by adding a CLI example._ +{{% /notice %}} + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `DODE_TOKEN` | API token | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here](/lego/dns/#configuration-and-credentials). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `DODE_HTTP_TIMEOUT` | API request timeout | +| `DODE_POLLING_INTERVAL` | Time between DNS propagation check | +| `DODE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `DODE_SEQUENCE_INTERVAL` | Interval between iteration | +| `DODE_TTL` | The TTL of the TXT record used for the DNS challenge | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here](/lego/dns/#configuration-and-credentials). + + + + +## More information + +- [API documentation](https://www.do.de/wiki/LetsEncrypt_-_Entwickler) + + + + diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 62bc40de..a8def091 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -19,6 +19,7 @@ import ( "github.com/go-acme/lego/providers/dns/dnsimple" "github.com/go-acme/lego/providers/dns/dnsmadeeasy" "github.com/go-acme/lego/providers/dns/dnspod" + "github.com/go-acme/lego/providers/dns/dode" "github.com/go-acme/lego/providers/dns/dreamhost" "github.com/go-acme/lego/providers/dns/duckdns" "github.com/go-acme/lego/providers/dns/dyn" @@ -91,6 +92,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return dnsmadeeasy.NewDNSProvider() case "dnspod": return dnspod.NewDNSProvider() + case "dode": + return dode.NewDNSProvider() case "dreamhost": return dreamhost.NewDNSProvider() case "duckdns": diff --git a/providers/dns/dode/client.go b/providers/dns/dode/client.go new file mode 100644 index 00000000..193b5160 --- /dev/null +++ b/providers/dns/dode/client.go @@ -0,0 +1,57 @@ +package dode + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/url" + + "github.com/go-acme/lego/challenge/dns01" +) + +type apiResponse struct { + Domain string + Success bool +} + +// updateTxtRecord Update the domains TXT record +// To update the TXT record we just need to make one simple get request. +func (d *DNSProvider) updateTxtRecord(fqdn, token, txt string, clear bool) error { + u, _ := url.Parse("https://www.do.de/api/letsencrypt") + + query := u.Query() + query.Set("token", token) + query.Set("domain", dns01.UnFqdn(fqdn)) + + // api call differs per set/delete + if clear { + query.Set("action", "delete") + } else { + query.Set("value", txt) + } + + u.RawQuery = query.Encode() + + response, err := d.config.HTTPClient.Get(u.String()) + if err != nil { + return err + } + defer response.Body.Close() + + bodyBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + + var r apiResponse + err = json.Unmarshal(bodyBytes, &r) + if err != nil { + return fmt.Errorf("request to change TXT record for do.de returned the following invalid json (%s); used url [%s]", string(bodyBytes), u) + } + + body := string(bodyBytes) + if !r.Success { + return fmt.Errorf("request to change TXT record for do.de returned the following error result (%s); used url [%s]", body, u) + } + return nil +} diff --git a/providers/dns/dode/dode.go b/providers/dns/dode/dode.go new file mode 100644 index 00000000..693543a7 --- /dev/null +++ b/providers/dns/dode/dode.go @@ -0,0 +1,89 @@ +// Package dode implements a DNS provider for solving the DNS-01 challenge using do.de. +package dode + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/challenge/dns01" + "github.com/go-acme/lego/platform/config/env" +) + +// Config is used to configure the creation of the DNSProvider +type Config struct { + Token string + PropagationTimeout time.Duration + PollingInterval time.Duration + SequenceInterval time.Duration + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond("DODE_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("DODE_POLLING_INTERVAL", dns01.DefaultPollingInterval), + SequenceInterval: env.GetOrDefaultSecond("DODE_SEQUENCE_INTERVAL", dns01.DefaultPropagationTimeout), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond("DODE_HTTP_TIMEOUT", 30*time.Second), + }, + } +} + +// DNSProvider adds and removes the record for the DNS challenge +type DNSProvider struct { + config *Config +} + +// NewDNSProvider returns a new DNS provider using +// environment variable DODE_TOKEN for adding and removing the DNS record. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get("DODE_TOKEN") + if err != nil { + return nil, fmt.Errorf("do.de: %v", err) + } + + config := NewDefaultConfig() + config.Token = values["DODE_TOKEN"] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for do.de. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("do.de: the configuration of the DNS provider is nil") + } + + if config.Token == "" { + return nil, errors.New("do.de: credentials missing") + } + + return &DNSProvider{config: config}, nil +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, txtRecord := dns01.GetRecord(domain, keyAuth) + return d.updateTxtRecord(fqdn, d.config.Token, txtRecord, false) +} + +// CleanUp clears TXT record +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _ := dns01.GetRecord(domain, keyAuth) + return d.updateTxtRecord(fqdn, d.config.Token, "", true) +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Sequential All DNS challenges for this provider will be resolved sequentially. +// Returns the interval between each iteration. +func (d *DNSProvider) Sequential() time.Duration { + return d.config.SequenceInterval +} diff --git a/providers/dns/dode/dode.toml b/providers/dns/dode/dode.toml new file mode 100644 index 00000000..fb9b99aa --- /dev/null +++ b/providers/dns/dode/dode.toml @@ -0,0 +1,19 @@ +Name = "Domain Offensive (do.de)" +Description = '''''' +URL = "https://www.do.de/" +Code = "dode" + +Example = '''''' + +[Configuration] + [Configuration.Credentials] + DODE_TOKEN = "API token" + [Configuration.Additional] + DODE_POLLING_INTERVAL = "Time between DNS propagation check" + DODE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + DODE_TTL = "The TTL of the TXT record used for the DNS challenge" + DODE_HTTP_TIMEOUT = "API request timeout" + DODE_SEQUENCE_INTERVAL = "Interval between iteration" + +[Links] + API = "https://www.do.de/wiki/LetsEncrypt_-_Entwickler" diff --git a/providers/dns/dode/dode_test.go b/providers/dns/dode/dode_test.go new file mode 100644 index 00000000..f0007030 --- /dev/null +++ b/providers/dns/dode/dode_test.go @@ -0,0 +1,115 @@ +package dode + +import ( + "testing" + "time" + + "github.com/go-acme/lego/platform/tester" + "github.com/stretchr/testify/require" +) + +var envTest = tester.NewEnvTest("DODE_TOKEN"). + WithDomain("DODE_DOMAIN") + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + "DODE_TOKEN": "123", + }, + }, + { + desc: "missing api key", + envVars: map[string]string{ + "DODE_TOKEN": "", + }, + expected: "do.de: some credentials information are missing: DODE_TOKEN", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if len(test.expected) == 0 { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + token string + expected string + }{ + { + desc: "success", + token: "123", + }, + { + desc: "missing credentials", + expected: "do.de: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Token = test.token + + p, err := NewDNSProviderConfig(config) + + if len(test.expected) == 0 { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + time.Sleep(1 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +}