diff --git a/cli.go b/cli.go index faacf748..bd61498e 100644 --- a/cli.go +++ b/cli.go @@ -208,6 +208,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN") fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_OAUTH_TOKEN") fmt.Fprintln(w, "\tdnsmadeeasy:\tDNSMADEEASY_API_KEY, DNSMADEEASY_API_SECRET") + fmt.Fprintln(w, "\tduckdns:\tDUCKDNS_TOKEN") fmt.Fprintln(w, "\texoscale:\tEXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT") fmt.Fprintln(w, "\tgandi:\tGANDI_API_KEY") fmt.Fprintln(w, "\tgandiv5:\tGANDIV5_API_KEY") diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index d507be08..931e3a5b 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -13,6 +13,7 @@ import ( "github.com/xenolf/lego/providers/dns/dnsimple" "github.com/xenolf/lego/providers/dns/dnsmadeeasy" "github.com/xenolf/lego/providers/dns/dnspod" + "github.com/xenolf/lego/providers/dns/duckdns" "github.com/xenolf/lego/providers/dns/dyn" "github.com/xenolf/lego/providers/dns/exec" "github.com/xenolf/lego/providers/dns/exoscale" @@ -55,6 +56,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) provider, err = dnsmadeeasy.NewDNSProvider() case "dnspod": provider, err = dnspod.NewDNSProvider() + case "duckdns": + provider, err = duckdns.NewDNSProvider() case "dyn": provider, err = dyn.NewDNSProvider() case "exoscale": diff --git a/providers/dns/duckdns/duckdns.go b/providers/dns/duckdns/duckdns.go new file mode 100644 index 00000000..6e2102a7 --- /dev/null +++ b/providers/dns/duckdns/duckdns.go @@ -0,0 +1,82 @@ +// Adds lego support for http://duckdns.org . +// +// See http://www.duckdns.org/spec.jsp for more info on updating TXT records. +package duckdns + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + + "github.com/xenolf/lego/acme" +) + +// DNSProvider adds and removes the record for the DNS challenge +type DNSProvider struct { + // The duckdns api token + token string +} + +// NewDNSProvider returns a new DNS provider using +// environment variable DUCKDNS_TOKEN for adding and removing the DNS record. +func NewDNSProvider() (*DNSProvider, error) { + duckdnsToken := os.Getenv("DUCKDNS_TOKEN") + + return NewDNSProviderCredentials(duckdnsToken) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for http://duckdns.org . +func NewDNSProviderCredentials(duckdnsToken string) (*DNSProvider, error) { + if duckdnsToken == "" { + return nil, errors.New("environment variable DUCKDNS_TOKEN not set") + } + + return &DNSProvider{token: duckdnsToken}, nil +} + +// makeDuckdnsURL creates a url to clear the set or unset the TXT record. +// txt == "" will clear the TXT record. +func makeDuckdnsURL(domain, token, txt string) string { + requestBase := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s", domain, token) + if txt == "" { + return requestBase + "&clear=true" + } + return requestBase + "&txt=" + txt +} + +func issueDuckdnsRequest(url string) error { + response, err := acme.HTTPClient.Get(url) + if err != nil { + return err + } + defer response.Body.Close() + + bodyBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + body := string(bodyBytes) + if body != "OK" { + return fmt.Errorf("Request to change TXT record for duckdns returned the following result (%s) this does not match expectation (OK) used url [%s]", body, url) + } + return nil +} + +// Present creates a TXT record to fulfil the dns-01 challenge. +// In duckdns you only have one TXT record shared with +// the domain and all sub domains. +// +// To update the TXT record we just need to make one simple get request. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + _, txtRecord, _ := acme.DNS01Record(domain, keyAuth) + url := makeDuckdnsURL(domain, d.token, txtRecord) + return issueDuckdnsRequest(url) +} + +// CleanUp clears duckdns TXT record +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + url := makeDuckdnsURL(domain, d.token, "") + return issueDuckdnsRequest(url) +} diff --git a/providers/dns/duckdns/duckdns_test.go b/providers/dns/duckdns/duckdns_test.go new file mode 100644 index 00000000..f1afed4f --- /dev/null +++ b/providers/dns/duckdns/duckdns_test.go @@ -0,0 +1,65 @@ +package duckdns + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + duckdnsLiveTest bool + duckdnsToken string + duckdnsDomain string +) + +func init() { + duckdnsToken = os.Getenv("DUCKDNS_TOKEN") + duckdnsDomain = os.Getenv("DUCKDNS_DOMAIN") + if len(duckdnsDomain) > 0 && len(duckdnsDomain) > 0 { + duckdnsLiveTest = true + } +} + +func restoreDuckdnsEnv() { + os.Setenv("DUCKDNS_TOKEN", duckdnsToken) +} + +func TestNewDNSProviderValidEnv(t *testing.T) { + os.Setenv("DUCKDNS_TOKEN", "123") + _, err := NewDNSProvider() + assert.NoError(t, err) + restoreDuckdnsEnv() +} +func TestNewDNSProviderMissingCredErr(t *testing.T) { + os.Setenv("DUCKDNS_TOKEN", "") + _, err := NewDNSProvider() + assert.EqualError(t, err, "environment variable DUCKDNS_TOKEN not set") + restoreDuckdnsEnv() +} +func TestLiveDuckdnsPresent(t *testing.T) { + if !duckdnsLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.Present(duckdnsDomain, "", "123d==") + assert.NoError(t, err) +} + +func TestLiveDuckdnsCleanUp(t *testing.T) { + if !duckdnsLiveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 10) + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.CleanUp(duckdnsDomain, "", "123d==") + assert.NoError(t, err) +}