From 1c309c9c8097635f726c915c96c6dfbd3e9307ce Mon Sep 17 00:00:00 2001 From: tzakrajs Date: Fri, 1 Mar 2019 10:09:00 -0800 Subject: [PATCH] Add DNS Provider for ClouDNS.net (#813) --- CONTRIBUTING.md | 1 + cmd/cmd_dnshelp.go | 2 + providers/dns/cloudns/cloudns.go | 108 +++++++++ providers/dns/cloudns/cloudns_test.go | 150 +++++++++++++ providers/dns/cloudns/internal/client.go | 209 ++++++++++++++++++ providers/dns/cloudns/internal/client_test.go | 194 ++++++++++++++++ providers/dns/cloudxns/cloudxns.go | 2 +- providers/dns/dns_providers.go | 3 + 8 files changed, 668 insertions(+), 1 deletion(-) create mode 100644 providers/dns/cloudns/cloudns.go create mode 100644 providers/dns/cloudns/cloudns_test.go create mode 100644 providers/dns/cloudns/internal/client.go create mode 100644 providers/dns/cloudns/internal/client_test.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1012bf00..8a4afbd5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,6 +85,7 @@ git push -u origin my-feature | Azure | `azure` | [documentation](https://docs.microsoft.com/en-us/go/azure/) | [Go client](https://github.com/Azure/azure-sdk-for-go) | | Bluecat | `bluecat` | ? | - | | Cloudflare | `cloudflare` | [documentation](https://api.cloudflare.com/) | [Go client](https://github.com/cloudflare/cloudflare-go) | +| ClouDNS | `cloudns` | [documentation](https://www.cloudns.net/wiki/article/42/) | - | | CloudXNS | `cloudxns` | [documentation](https://www.cloudxns.net/Public/Doc/CloudXNS_api2.0_doc_zh-cn.zip) | - | | ConoHa | `conoha` | [documentation](https://www.conoha.jp/docs/) | - | | Openstack Designate | `designate` | [documentation](https://docs.openstack.org/designate/latest/) | [Go client](https://godoc.org/github.com/gophercloud/gophercloud/openstack/dns/v2) | diff --git a/cmd/cmd_dnshelp.go b/cmd/cmd_dnshelp.go index 2e344c74..e54058aa 100644 --- a/cmd/cmd_dnshelp.go +++ b/cmd/cmd_dnshelp.go @@ -37,6 +37,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tazure:\tAZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP") fmt.Fprintln(w, "\tbluecat:\tBLUECAT_SERVER_URL, BLUECAT_USER_NAME, BLUECAT_PASSWORD, BLUECAT_CONFIG_NAME, BLUECAT_DNS_VIEW") fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY") + fmt.Fprintln(w, "\tcloudns:\tCLOUDNS_AUTH_ID, CLOUDNS_AUTH_PASSWORD") fmt.Fprintln(w, "\tcloudxns:\tCLOUDXNS_API_KEY, CLOUDXNS_SECRET_KEY") fmt.Fprintln(w, "\tconoha:\tCONOHA_TENANT_ID, CONOHA_API_USERNAME, CONOHA_API_PASSWORD") fmt.Fprintln(w, "\tdesignate:\tOS_AUTH_URL, OS_USERNAME, OS_PASSWORD, OS_TENANT_NAME, OS_REGION_NAME") @@ -92,6 +93,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tazure:\tAZURE_POLLING_INTERVAL, AZURE_PROPAGATION_TIMEOUT, AZURE_TTL, AZURE_METADATA_ENDPOINT") fmt.Fprintln(w, "\tbluecat:\tBLUECAT_POLLING_INTERVAL, BLUECAT_PROPAGATION_TIMEOUT, BLUECAT_TTL, BLUECAT_HTTP_TIMEOUT") fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_POLLING_INTERVAL, CLOUDFLARE_PROPAGATION_TIMEOUT, CLOUDFLARE_TTL, CLOUDFLARE_HTTP_TIMEOUT") + fmt.Fprintln(w, "\tcloudns:\tCLOUDNS_POLLING_INTERVAL, CLOUDNS_PROPAGATION_TIMEOUT, CLOUDNS_TTL, CLOUDNS_HTTP_TIMEOUT") fmt.Fprintln(w, "\tcloudxns:\tCLOUDXNS_POLLING_INTERVAL, CLOUDXNS_PROPAGATION_TIMEOUT, CLOUDXNS_TTL, CLOUDXNS_HTTP_TIMEOUT") fmt.Fprintln(w, "\tconoha:\tCONOHA_POLLING_INTERVAL, CONOHA_PROPAGATION_TIMEOUT, CONOHA_TTL, CONOHA_HTTP_TIMEOUT, CONOHA_REGION") fmt.Fprintln(w, "\tdesignate:\tDESIGNATE_POLLING_INTERVAL, DESIGNATE_PROPAGATION_TIMEOUT, DESIGNATE_TTL") diff --git a/providers/dns/cloudns/cloudns.go b/providers/dns/cloudns/cloudns.go new file mode 100644 index 00000000..6b4aaaa8 --- /dev/null +++ b/providers/dns/cloudns/cloudns.go @@ -0,0 +1,108 @@ +// Package cloudns implements a DNS provider for solving the DNS-01 challenge using ClouDNS DNS. +package cloudns + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/xenolf/lego/challenge/dns01" + "github.com/xenolf/lego/platform/config/env" + "github.com/xenolf/lego/providers/dns/cloudns/internal" +) + +// Config is used to configure the creation of the DNSProvider +type Config struct { + AuthID string + AuthPassword string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond("CLOUDNS_PROPAGATION_TIMEOUT", 120*time.Second), + PollingInterval: env.GetOrDefaultSecond("CLOUDNS_POLLING_INTERVAL", 4*time.Second), + TTL: env.GetOrDefaultInt("CLOUDNS_TTL", dns01.DefaultTTL), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond("CLOUDNS_HTTP_TIMEOUT", 30*time.Second), + }, + } +} + +// DNSProvider is an implementation of the acme.ChallengeProvider interface +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for ClouDNS. +// Credentials must be passed in the environment variables: +// CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get("CLOUDNS_AUTH_ID", "CLOUDNS_AUTH_PASSWORD") + if err != nil { + return nil, fmt.Errorf("ClouDNS: %v", err) + } + + config := NewDefaultConfig() + config.AuthID = values["CLOUDNS_AUTH_ID"] + config.AuthPassword = values["CLOUDNS_AUTH_PASSWORD"] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for ClouDNS. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("ClouDNS: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.AuthID, config.AuthPassword) + if err != nil { + return nil, err + } + + client.HTTPClient = config.HTTPClient + + return &DNSProvider{client: client, config: config}, 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) + + zone, err := d.client.GetZone(fqdn) + if err != nil { + return err + } + + return d.client.AddTxtRecord(zone.Name, fqdn, value, d.config.TTL) +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _ := dns01.GetRecord(domain, keyAuth) + + zone, err := d.client.GetZone(fqdn) + if err != nil { + return err + } + + record, err := d.client.FindTxtRecord(zone.Name, fqdn) + if err != nil { + return err + } + + return d.client.RemoveTxtRecord(record.ID, zone.Name) +} + +// 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 +} diff --git a/providers/dns/cloudns/cloudns_test.go b/providers/dns/cloudns/cloudns_test.go new file mode 100644 index 00000000..0a1da286 --- /dev/null +++ b/providers/dns/cloudns/cloudns_test.go @@ -0,0 +1,150 @@ +package cloudns + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/platform/tester" +) + +var envTest = tester.NewEnvTest( + "CLOUDNS_AUTH_ID", + "CLOUDNS_AUTH_PASSWORD"). + WithDomain("CLOUDNS_DOMAIN") + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + "CLOUDNS_AUTH_ID": "123", + "CLOUDNS_AUTH_PASSWORD": "456", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{ + "CLOUDNS_AUTH_ID": "", + "CLOUDNS_AUTH_PASSWORD": "", + }, + expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID,CLOUDNS_AUTH_PASSWORD", + }, + { + desc: "missing auth-id", + envVars: map[string]string{ + "CLOUDNS_AUTH_ID": "", + "CLOUDNS_AUTH_PASSWORD": "456", + }, + expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID", + }, + { + desc: "missing auth-password", + envVars: map[string]string{ + "CLOUDNS_AUTH_ID": "123", + "CLOUDNS_AUTH_PASSWORD": "", + }, + expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_PASSWORD", + }, + } + + 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) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + authID string + authPassword string + expected string + }{ + { + desc: "success", + authID: "123", + authPassword: "456", + }, + { + desc: "missing credentials", + expected: "ClouDNS: credentials missing: authID", + }, + { + desc: "missing auth-id", + authPassword: "456", + expected: "ClouDNS: credentials missing: authID", + }, + { + desc: "missing auth-password", + authID: "123", + expected: "ClouDNS: credentials missing: authPassword", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.AuthID = test.authID + config.AuthPassword = test.authPassword + + p, err := NewDNSProviderConfig(config) + + if len(test.expected) == 0 { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } 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(2 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/cloudns/internal/client.go b/providers/dns/cloudns/internal/client.go new file mode 100644 index 00000000..c3df3658 --- /dev/null +++ b/providers/dns/cloudns/internal/client.go @@ -0,0 +1,209 @@ +package internal + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/xenolf/lego/challenge/dns01" +) + +const defaultBaseURL = "https://api.cloudns.net/dns/" + +type Zone struct { + Name string + Type string + Zone string + Status string // is an integer, but cast as string +} + +// TXTRecord a TXT record +type TXTRecord struct { + ID int `json:"id,string"` + Type string `json:"type"` + Host string `json:"host"` + Record string `json:"record"` + Failover int `json:"failover,string"` + TTL int `json:"ttl,string"` + Status int `json:"status"` +} + +type TXTRecords map[string]TXTRecord + +// NewClient creates a ClouDNS client +func NewClient(authID string, authPassword string) (*Client, error) { + if authID == "" { + return nil, fmt.Errorf("ClouDNS: credentials missing: authID") + } + + if authPassword == "" { + return nil, fmt.Errorf("ClouDNS: credentials missing: authPassword") + } + + baseURL, err := url.Parse(defaultBaseURL) + if err != nil { + return nil, err + } + + return &Client{ + authID: authID, + authPassword: authPassword, + HTTPClient: &http.Client{}, + BaseURL: baseURL, + }, nil +} + +// Client ClouDNS client +type Client struct { + authID string + authPassword string + HTTPClient *http.Client + BaseURL *url.URL +} + +// GetZone Get domain name information for a FQDN +func (c *Client) GetZone(authFQDN string) (*Zone, error) { + authZone, err := dns01.FindZoneByFqdn(authFQDN) + if err != nil { + return nil, err + } + + authZoneName := dns01.UnFqdn(authZone) + + reqURL := *c.BaseURL + reqURL.Path += "get-zone-info.json" + + q := reqURL.Query() + q.Add("domain-name", authZoneName) + reqURL.RawQuery = q.Encode() + + result, err := c.doRequest(http.MethodGet, &reqURL) + if err != nil { + return nil, err + } + + var zone Zone + + if len(result) > 0 { + if err = json.Unmarshal(result, &zone); err != nil { + return nil, fmt.Errorf("ClouDNS: zone unmarshaling error: %v", err) + } + } + + if zone.Name == authZoneName { + return &zone, nil + } + + return nil, fmt.Errorf("ClouDNS: zone %s not found for authFQDN %s", authZoneName, authFQDN) +} + +// FindTxtRecord return the TXT record a zone ID and a FQDN +func (c *Client) FindTxtRecord(zoneName, fqdn string) (*TXTRecord, error) { + host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName)) + + reqURL := *c.BaseURL + reqURL.Path += "records.json" + + q := reqURL.Query() + q.Add("domain-name", zoneName) + q.Add("host", host) + q.Add("type", "TXT") + reqURL.RawQuery = q.Encode() + + result, err := c.doRequest(http.MethodGet, &reqURL) + if err != nil { + return nil, err + } + + var records TXTRecords + if err = json.Unmarshal(result, &records); err != nil { + return nil, fmt.Errorf("ClouDNS: TXT record unmarshaling error: %v", err) + } + + for _, record := range records { + if record.Host == host && record.Type == "TXT" { + return &record, nil + } + } + + return nil, fmt.Errorf("ClouDNS: no existing record found for %q", fqdn) +} + +// AddTxtRecord add a TXT record +func (c *Client) AddTxtRecord(zoneName string, fqdn, value string, ttl int) error { + host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName)) + + reqURL := *c.BaseURL + reqURL.Path += "add-record.json" + + q := reqURL.Query() + q.Add("domain-name", zoneName) + q.Add("host", host) + q.Add("record", value) + q.Add("ttl", strconv.Itoa(ttl)) + q.Add("record-type", "TXT") + reqURL.RawQuery = q.Encode() + + _, err := c.doRequest(http.MethodPost, &reqURL) + return err +} + +// RemoveTxtRecord remove a TXT record +func (c *Client) RemoveTxtRecord(recordID int, zoneName string) error { + reqURL := *c.BaseURL + reqURL.Path += "delete-record.json" + + q := reqURL.Query() + q.Add("domain-name", zoneName) + q.Add("record-id", strconv.Itoa(recordID)) + reqURL.RawQuery = q.Encode() + + _, err := c.doRequest(http.MethodPost, &reqURL) + return err +} + +func (c *Client) doRequest(method string, url *url.URL) (json.RawMessage, error) { + req, err := c.buildRequest(method, url) + if err != nil { + return nil, err + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("ClouDNS: %v", err) + } + + defer resp.Body.Close() + + content, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("ClouDNS: %s", toUnreadableBodyMessage(req, content)) + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("ClouDNS: invalid code (%v), error: %s", resp.StatusCode, content) + } + return content, nil +} + +func (c *Client) buildRequest(method string, url *url.URL) (*http.Request, error) { + q := url.Query() + q.Add("auth-id", c.authID) + q.Add("auth-password", c.authPassword) + url.RawQuery = q.Encode() + + req, err := http.NewRequest(method, url.String(), nil) + if err != nil { + return nil, fmt.Errorf("ClouDNS: invalid request: %v", err) + } + + return req, nil +} + +func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { + return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody)) +} diff --git a/providers/dns/cloudns/internal/client_test.go b/providers/dns/cloudns/internal/client_test.go new file mode 100644 index 00000000..674e8a6c --- /dev/null +++ b/providers/dns/cloudns/internal/client_test.go @@ -0,0 +1,194 @@ +package internal + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func handlerMock(method string, jsonData []byte) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, "Incorrect method used", http.StatusBadRequest) + return + } + + _, err := rw.Write(jsonData) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) +} + +func TestClientGetZone(t *testing.T) { + type result struct { + zone *Zone + error bool + } + testCases := []struct { + desc string + authFQDN string + apiResponse []byte + expected result + }{ + { + desc: "zone found", + authFQDN: "_acme-challenge.foo.com.", + apiResponse: []byte(`{"name": "foo.com", "type": "master", "zone": "zone", "status": "1"}`), + expected: result{ + zone: &Zone{ + Name: "foo.com", + Type: "master", + Zone: "zone", + Status: "1", + }, + }, + }, + { + desc: "zone not found", + authFQDN: "_acme-challenge.foo.com.", + apiResponse: []byte(``), + expected: result{error: true}, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + server := httptest.NewServer(handlerMock(http.MethodGet, test.apiResponse)) + + client, _ := NewClient("myAuthID", "myAuthPassword") + mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL)) + client.BaseURL = mockBaseURL + + zone, err := client.GetZone(test.authFQDN) + + if test.expected.error { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.expected.zone, zone) + } + }) + } +} + +func TestClientFindTxtRecord(t *testing.T) { + type result struct { + txtRecord *TXTRecord + error bool + } + + testCases := []struct { + desc string + authFQDN string + zoneName string + apiResponse []byte + expected result + }{ + { + desc: "record found", + authFQDN: "_acme-challenge.foo.com.", + zoneName: "foo.com", + apiResponse: []byte(`{"1":{"id":"1","type":"TXT","host":"_acme-challenge","record":"txtTXTtxtTXTtxtTXTtxtTXT","failover":"1","ttl":"30","status":1}}`), + expected: result{ + txtRecord: &TXTRecord{ + ID: 1, + Type: "TXT", + Host: "_acme-challenge", + Record: "txtTXTtxtTXTtxtTXTtxtTXT", + Failover: 1, + TTL: 30, + Status: 1, + }, + }, + }, + { + desc: "record not found", + authFQDN: "_acme-challenge.foo.com.", + zoneName: "test-zone", + apiResponse: []byte(``), + expected: result{error: true}, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + server := httptest.NewServer(handlerMock(http.MethodGet, test.apiResponse)) + + client, _ := NewClient("myAuthID", "myAuthPassword") + mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL)) + client.BaseURL = mockBaseURL + + txtRecord, err := client.FindTxtRecord(test.zoneName, test.authFQDN) + + if test.expected.error { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.expected.txtRecord, txtRecord) + } + }) + } +} + +func TestClientAddTxtRecord(t *testing.T) { + testCases := []struct { + desc string + zone *Zone + authFQDN string + value string + ttl int + expected string + }{ + { + desc: "sub-zone", + zone: &Zone{ + Name: "bar.com", + Type: "master", + Zone: "domain", + Status: "1", + }, + authFQDN: "_acme-challenge.foo.bar.com.", + value: "txtTXTtxtTXTtxtTXTtxtTXT", + ttl: 60, + expected: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge.foo&record=txtTXTtxtTXTtxtTXTtxtTXT&record-type=TXT&ttl=60`, + }, + { + desc: "main zone", + zone: &Zone{ + Name: "bar.com", + Type: "master", + Zone: "domain", + Status: "1", + }, + authFQDN: "_acme-challenge.bar.com.", + value: "TXTtxtTXTtxtTXTtxtTXTtxt", + ttl: 60, + expected: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=60`, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.NotNil(t, req.URL.RawQuery) + assert.Equal(t, test.expected, req.URL.RawQuery) + + handlerMock(http.MethodPost, nil).ServeHTTP(rw, req) + })) + + client, _ := NewClient("myAuthID", "myAuthPassword") + mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL)) + client.BaseURL = mockBaseURL + + err := client.AddTxtRecord(test.zone.Name, test.authFQDN, test.value, test.ttl) + require.NoError(t, err) + }) + } +} diff --git a/providers/dns/cloudxns/cloudxns.go b/providers/dns/cloudxns/cloudxns.go index 0a3a7d89..5fad1db4 100644 --- a/providers/dns/cloudxns/cloudxns.go +++ b/providers/dns/cloudxns/cloudxns.go @@ -29,7 +29,7 @@ func NewDefaultConfig() *Config { PollingInterval: env.GetOrDefaultSecond("CLOUDXNS_POLLING_INTERVAL", dns01.DefaultPollingInterval), TTL: env.GetOrDefaultInt("CLOUDXNS_TTL", dns01.DefaultTTL), HTTPClient: &http.Client{ - Timeout: time.Second * time.Duration(env.GetOrDefaultInt("CLOUDXNS_HTTP_TIMEOUT", 30)), + Timeout: env.GetOrDefaultSecond("CLOUDXNS_HTTP_TIMEOUT", 30*time.Second), }, } } diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 979fa03e..2846ee58 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -11,6 +11,7 @@ import ( "github.com/xenolf/lego/providers/dns/azure" "github.com/xenolf/lego/providers/dns/bluecat" "github.com/xenolf/lego/providers/dns/cloudflare" + "github.com/xenolf/lego/providers/dns/cloudns" "github.com/xenolf/lego/providers/dns/cloudxns" "github.com/xenolf/lego/providers/dns/conoha" "github.com/xenolf/lego/providers/dns/designate" @@ -74,6 +75,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return bluecat.NewDNSProvider() case "cloudflare": return cloudflare.NewDNSProvider() + case "cloudns": + return cloudns.NewDNSProvider() case "cloudxns": return cloudxns.NewDNSProvider() case "conoha":