diff --git a/Makefile b/Makefile index 75c254ae..473af5d2 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,11 @@ SRCS = $(shell git ls-files '*.go' | grep -v '^vendor/') LEGO_IMAGE := go-acme/lego MAIN_DIRECTORY := ./cmd/lego/ -BIN_OUTPUT := dist/lego +ifeq (${GOOS}, windows) + BIN_OUTPUT := dist/lego.exe +else + BIN_OUTPUT := dist/lego +endif TAG_NAME := $(shell git tag -l --contains HEAD) SHA := $(shell git rev-parse HEAD) diff --git a/README.md b/README.md index aee6eb0b..be621b0a 100644 --- a/README.md +++ b/README.md @@ -48,13 +48,13 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). | [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/) | -| [Joker](https://go-acme.github.io/lego/dns/joker/) | [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/) | | | | +| [EasyDNS](https://go-acme.github.io/lego/dns/easydns/) | [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/) | [Joker](https://go-acme.github.io/lego/dns/joker/) | [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 ea9aadc3..bbd932fe 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -35,6 +35,7 @@ func allDNSCodes() string { "dreamhost", "duckdns", "dyn", + "easydns", "exec", "exoscale", "fastdns", @@ -481,6 +482,29 @@ func displayDNSHelp(name string) { fmt.Fprintln(w) fmt.Fprintln(w, `More information: https://go-acme.github.io/lego/dns/dyn`) + case "easydns": + // generated from: providers/dns/easydns/easydns.toml + fmt.Fprintln(w, `Configuration for EasyDNS.`) + fmt.Fprintln(w, `Code: 'easydns'`) + fmt.Fprintln(w, `Since: 'v2.6.0'`) + fmt.Fprintln(w) + + fmt.Fprintln(w, `Credentials:`) + fmt.Fprintln(w, ` - "EASYDNS_KEY": API Key`) + fmt.Fprintln(w, ` - "EASYDNS_TOKEN": API Token`) + fmt.Fprintln(w) + + fmt.Fprintln(w, `Additional Configuration:`) + fmt.Fprintln(w, ` - "EASYDNS_ENDPOINT": The endpoint URL of the API Server`) + fmt.Fprintln(w, ` - "EASYDNS_HTTP_TIMEOUT": API request timeout`) + fmt.Fprintln(w, ` - "EASYDNS_POLLING_INTERVAL": Time between DNS propagation check`) + fmt.Fprintln(w, ` - "EASYDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + fmt.Fprintln(w, ` - "EASYDNS_SEQUENCE_INTERVAL": Time between sequential requests`) + fmt.Fprintln(w, ` - "EASYDNS_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/easydns`) + case "exec": // generated from: providers/dns/exec/exec.toml fmt.Fprintln(w, `Configuration for External program.`) diff --git a/docs/content/dns/zz_gen_easydns.md b/docs/content/dns/zz_gen_easydns.md new file mode 100644 index 00000000..22f963c2 --- /dev/null +++ b/docs/content/dns/zz_gen_easydns.md @@ -0,0 +1,67 @@ +--- +title: "EasyDNS" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: easydns +--- + + + + + +Since: v2.6.0 + +Configuration for [EasyDNS](https://easydns.com/). + + + + +- Code: `easydns` + +Here is an example bash command using the EasyDNS provider: + +```bash +EASYDNS_TOKEN= \ +EASYDNS_KEY= \ +lego --dns easydns --domains my.domain.com --email my@email.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `EASYDNS_KEY` | API Key | +| `EASYDNS_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 | +|--------------------------------|-------------| +| `EASYDNS_ENDPOINT` | The endpoint URL of the API Server | +| `EASYDNS_HTTP_TIMEOUT` | API request timeout | +| `EASYDNS_POLLING_INTERVAL` | Time between DNS propagation check | +| `EASYDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `EASYDNS_SEQUENCE_INTERVAL` | Time between sequential requests | +| `EASYDNS_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). + +To test with the sandbox environment set ```EASYDNS_ENDPOINT=https://sandbox.rest.easydns.net``` + + + +## More information + +- [API documentation](http://docs.sandbox.rest.easydns.net) + + + + diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index e9a4c6d2..194db1d3 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -24,6 +24,7 @@ import ( "github.com/go-acme/lego/providers/dns/dreamhost" "github.com/go-acme/lego/providers/dns/duckdns" "github.com/go-acme/lego/providers/dns/dyn" + "github.com/go-acme/lego/providers/dns/easydns" "github.com/go-acme/lego/providers/dns/exec" "github.com/go-acme/lego/providers/dns/exoscale" "github.com/go-acme/lego/providers/dns/fastdns" @@ -106,6 +107,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return dyn.NewDNSProvider() case "fastdns": return fastdns.NewDNSProvider() + case "easydns": + return easydns.NewDNSProvider() case "exec": return exec.NewDNSProvider() case "exoscale": diff --git a/providers/dns/easydns/client.go b/providers/dns/easydns/client.go new file mode 100644 index 00000000..3d2c565b --- /dev/null +++ b/providers/dns/easydns/client.go @@ -0,0 +1,97 @@ +package easydns + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "path" +) + +const defaultEndpoint = "https://rest.easydns.net" + +type zoneRecord struct { + ID string `json:"id,omitempty"` + Domain string `json:"domain"` + Host string `json:"host"` + TTL string `json:"ttl"` + Prio string `json:"prio"` + Type string `json:"type"` + Rdata string `json:"rdata"` + LastMod string `json:"last_mod,omitempty"` + Revoked int `json:"revoked,omitempty"` + NewHost string `json:"new_host,omitempty"` +} + +type addRecordResponse struct { + Msg string `json:"msg"` + Tm int `json:"tm"` + Data zoneRecord `json:"data"` + Status int `json:"status"` +} + +func (d *DNSProvider) addRecord(domain string, record interface{}) (string, error) { + pathAdd := path.Join("/zones/records/add", domain, "TXT") + + response := &addRecordResponse{} + err := d.doRequest(http.MethodPut, pathAdd, record, response) + if err != nil { + return "", err + } + + recordID := response.Data.ID + + return recordID, nil +} + +func (d *DNSProvider) deleteRecord(domain, recordID string) error { + pathDelete := path.Join("/zones/records", domain, recordID) + + return d.doRequest(http.MethodDelete, pathDelete, nil, nil) +} + +func (d *DNSProvider) doRequest(method, path string, requestMsg, responseMsg interface{}) error { + reqBody := &bytes.Buffer{} + if requestMsg != nil { + err := json.NewEncoder(reqBody).Encode(requestMsg) + if err != nil { + return err + } + } + + endpoint, err := d.config.Endpoint.Parse(path + "?format=json") + if err != nil { + return err + } + + request, err := http.NewRequest(method, endpoint.String(), reqBody) + if err != nil { + return err + } + + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Accept", "application/json") + request.SetBasicAuth(d.config.Token, d.config.Key) + + response, err := d.config.HTTPClient.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode >= http.StatusBadRequest { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return fmt.Errorf("%d: failed to read response body: %v", response.StatusCode, err) + } + + return fmt.Errorf("%d: request failed: %v", response.StatusCode, string(body)) + } + + if responseMsg != nil { + return json.NewDecoder(response.Body).Decode(responseMsg) + } + + return nil +} diff --git a/providers/dns/easydns/easydns.go b/providers/dns/easydns/easydns.go new file mode 100644 index 00000000..2ef5c285 --- /dev/null +++ b/providers/dns/easydns/easydns.go @@ -0,0 +1,165 @@ +// Package easydns implements a DNS provider for solving the DNS-01 challenge using EasyDNS API. +package easydns + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "github.com/miekg/dns" + + "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 { + Endpoint *url.URL + Token string + Key string + TTL int + HTTPClient *http.Client + PropagationTimeout time.Duration + PollingInterval time.Duration + SequenceInterval time.Duration +} + +// NewDefaultConfig returns a default configuration for the DNSProvider +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond("EASYDNS_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + SequenceInterval: env.GetOrDefaultSecond("EASYDNS_SEQUENCE_INTERVAL", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("EASYDNS_POLLING_INTERVAL", dns01.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("EASYDNS_TTL", dns01.DefaultTTL), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond("EASYDNS_HTTP_TIMEOUT", 30*time.Second), + }, + } +} + +// DNSProvider describes a provider for acme-proxy +type DNSProvider struct { + config *Config + recordIDs map[string]string + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance. +func NewDNSProvider() (*DNSProvider, error) { + config := NewDefaultConfig() + + endpoint, err := url.Parse(env.GetOrDefaultString("EASYDNS_ENDPOINT", defaultEndpoint)) + if err != nil { + return nil, fmt.Errorf("easydns: %v", err) + } + config.Endpoint = endpoint + + values, err := env.Get("EASYDNS_TOKEN", "EASYDNS_KEY") + if err != nil { + return nil, fmt.Errorf("easydns: %v", err) + } + + config.Token = values["EASYDNS_TOKEN"] + config.Key = values["EASYDNS_KEY"] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider . +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("easydns: the configuration of the DNS provider is nil") + } + + if config.Token == "" { + return nil, errors.New("easydns: the API token is missing") + } + + if config.Key == "" { + return nil, errors.New("easydns: the API key is missing") + } + + return &DNSProvider{config: config, recordIDs: map[string]string{}}, nil +} + +// Present creates a TXT record to fulfill the dns-01 challenge +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + + apiHost, apiDomain := splitFqdn(fqdn) + record := &zoneRecord{ + Domain: apiDomain, + Host: apiHost, + Type: "TXT", + Rdata: value, + TTL: strconv.Itoa(d.config.TTL), + Prio: "0", + } + + recordID, err := d.addRecord(apiDomain, record) + if err != nil { + return fmt.Errorf("easydns: error adding zone record: %v", err) + } + + key := getMapKey(fqdn, value) + + d.recordIDsMu.Lock() + d.recordIDs[key] = recordID + d.recordIDsMu.Unlock() + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, challenge := dns01.GetRecord(domain, keyAuth) + + key := getMapKey(fqdn, challenge) + recordID, exists := d.recordIDs[key] + if !exists { + return nil + } + + _, apiDomain := splitFqdn(fqdn) + err := d.deleteRecord(apiDomain, recordID) + + d.recordIDsMu.Lock() + defer delete(d.recordIDs, key) + d.recordIDsMu.Unlock() + + if err != nil { + return fmt.Errorf("easydns: %v", err) + } + + return nil +} + +// 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 +} + +func splitFqdn(fqdn string) (host, domain string) { + parts := dns.SplitDomainName(fqdn) + length := len(parts) + + host = strings.Join(parts[0:length-2], ".") + domain = strings.Join(parts[length-2:length], ".") + return +} + +func getMapKey(fqdn, value string) string { + return fqdn + "|" + value +} diff --git a/providers/dns/easydns/easydns.toml b/providers/dns/easydns/easydns.toml new file mode 100644 index 00000000..01e866e6 --- /dev/null +++ b/providers/dns/easydns/easydns.toml @@ -0,0 +1,30 @@ +Name = "EasyDNS" +Description = '''''' +URL = "https://easydns.com/" +Code = "easydns" +Since = "v2.6.0" + +Example = ''' +EASYDNS_TOKEN= \ +EASYDNS_KEY= \ +lego --dns easydns --domains my.domain.com --email my@email.com run +''' + +Additional = ''' +To test with the sandbox environment set ```EASYDNS_ENDPOINT=https://sandbox.rest.easydns.net``` +''' + +[Configuration] + [Configuration.Credentials] + EASYDNS_TOKEN = "API Token" + EASYDNS_KEY = "API Key" + [Configuration.Additional] + EASYDNS_ENDPOINT = "The endpoint URL of the API Server" + EASYDNS_POLLING_INTERVAL = "Time between DNS propagation check" + EASYDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + EASYDNS_SEQUENCE_INTERVAL = "Time between sequential requests" + EASYDNS_TTL = "The TTL of the TXT record used for the DNS challenge" + EASYDNS_HTTP_TIMEOUT = "API request timeout" + +[Links] + API = "http://docs.sandbox.rest.easydns.net" diff --git a/providers/dns/easydns/easydns_test.go b/providers/dns/easydns/easydns_test.go new file mode 100644 index 00000000..b4412ee2 --- /dev/null +++ b/providers/dns/easydns/easydns_test.go @@ -0,0 +1,315 @@ +package easydns + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-acme/lego/platform/tester" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var envTest = tester.NewEnvTest( + "EASYDNS_ENDPOINT", + "EASYDNS_TOKEN", + "EASYDNS_KEY"). + WithDomain("EASYDNS_DOMAIN") + +func setup() (*DNSProvider, *http.ServeMux, func()) { + handler := http.NewServeMux() + server := httptest.NewServer(handler) + + endpoint, err := url.Parse(server.URL) + if err != nil { + panic(err) + } + + config := NewDefaultConfig() + config.Token = "TOKEN" + config.Key = "SECRET" + config.Endpoint = endpoint + + provider, err := NewDNSProviderConfig(config) + if err != nil { + panic(err) + } + + return provider, handler, server.Close +} + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + "EASYDNS_TOKEN": "TOKEN", + "EASYDNS_KEY": "SECRET", + }, + }, + { + desc: "missing token", + envVars: map[string]string{ + "EASYDNS_KEY": "SECRET", + }, + expected: "easydns: some credentials information are missing: EASYDNS_TOKEN", + }, + { + desc: "missing key", + envVars: map[string]string{ + "EASYDNS_TOKEN": "TOKEN", + }, + expected: "easydns: some credentials information are missing: EASYDNS_KEY", + }, + } + + 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 + config *Config + expected string + }{ + { + desc: "success", + config: &Config{ + Token: "TOKEN", + Key: "KEY", + }, + }, + { + desc: "nil config", + config: nil, + expected: "easydns: the configuration of the DNS provider is nil", + }, + { + desc: "missing token", + config: &Config{ + Key: "KEY", + }, + expected: "easydns: the API token is missing", + }, + { + desc: "missing key", + config: &Config{ + Token: "TOKEN", + }, + expected: "easydns: the API key is missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + p, err := NewDNSProviderConfig(test.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 TestDNSProvider_Present(t *testing.T) { + provider, mux, tearDown := setup() + defer tearDown() + + mux.HandleFunc("/zones/records/add/example.com/TXT", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method, "method") + assert.Equal(t, "format=json", r.URL.RawQuery, "query") + assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type") + assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get("Authorization"), "Authorization") + + reqBody, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + expectedReqBody := `{"domain":"example.com","host":"_acme-challenge","ttl":"120","prio":"0","type":"TXT","rdata":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"} +` + assert.Equal(t, expectedReqBody, string(reqBody)) + + w.WriteHeader(http.StatusCreated) + _, err = fmt.Fprintf(w, `{ + "msg": "OK", + "tm": 1554681934, + "data": { + "host": "_acme-challenge", + "geozone_id": 0, + "ttl": "120", + "prio": "0", + "rdata": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM", + "revoked": 0, + "id": "123456789", + "new_host": "_acme-challenge.example.com" + }, + "status": 201 + }`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + err := provider.Present("example.com", "token", "keyAuth") + require.NoError(t, err) + require.Contains(t, provider.recordIDs, "_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM") +} + +func TestDNSProvider_Cleanup_WhenRecordIdNotSet_NoOp(t *testing.T) { + provider, _, tearDown := setup() + defer tearDown() + + err := provider.CleanUp("example.com", "token", "keyAuth") + require.NoError(t, err) +} + +func TestDNSProvider_Cleanup_WhenRecordIdSet_DeletesTxtRecord(t *testing.T) { + provider, mux, tearDown := setup() + defer tearDown() + + mux.HandleFunc("/zones/records/example.com/123456", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method, "method") + assert.Equal(t, "format=json", r.URL.RawQuery, "query") + assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get("Authorization"), "Authorization") + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprintf(w, `{ + "msg": "OK", + "data": { + "domain": "example.com", + "id": "123456" + }, + "status": 200 + }`) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + provider.recordIDs["_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"] = "123456" + err := provider.CleanUp("example.com", "token", "keyAuth") + require.NoError(t, err) +} + +func TestDNSProvider_Cleanup_WhenHttpError_ReturnsError(t *testing.T) { + provider, mux, tearDown := setup() + defer tearDown() + + errorMessage := `{ + "error": { + "code": 406, + "message": "Provided id is invalid or you do not have permission to access it." + } + }` + mux.HandleFunc("/zones/records/example.com/123456", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method, "method") + assert.Equal(t, "format=json", r.URL.RawQuery, "query") + assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get("Authorization"), "Authorization") + + w.WriteHeader(http.StatusNotAcceptable) + _, err := fmt.Fprintf(w, errorMessage) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + provider.recordIDs["_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"] = "123456" + err := provider.CleanUp("example.com", "token", "keyAuth") + expectedError := fmt.Sprintf("easydns: 406: request failed: %v", errorMessage) + require.EqualError(t, err, expectedError) +} + +func TestSplitFqdn(t *testing.T) { + testCases := []struct { + desc string + fqdn string + expectedHost string + expectedDomain string + }{ + { + desc: "domain only", + fqdn: "domain.com.", + expectedHost: "", + expectedDomain: "domain.com", + }, + { + desc: "single-part host", + fqdn: "_acme-challenge.domain.com.", + expectedHost: "_acme-challenge", + expectedDomain: "domain.com", + }, + { + desc: "multi-part host", + fqdn: "_acme-challenge.sub.domain.com.", + expectedHost: "_acme-challenge.sub", + expectedDomain: "domain.com", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + actualHost, actualDomain := splitFqdn(test.fqdn) + + require.Equal(t, test.expectedHost, actualHost) + require.Equal(t, test.expectedDomain, actualDomain) + }) + } +} + +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(2 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +}