From 1aeac60ab36e097fed346401f6de0e84c5ae9b92 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Sat, 14 Mar 2020 13:32:50 +0100 Subject: [PATCH] Add DNS provider for Zonomi and RimuHosting. (#1084) --- README.md | 7 +- cmd/zz_gen_cmd_dnshelp.go | 42 +++ docs/content/dns/zz_gen_rimuhosting.md | 62 ++++ docs/content/dns/zz_gen_zonomi.md | 62 ++++ providers/dns/dns_providers.go | 10 +- providers/dns/internal/rimuhosting/client.go | 178 ++++++++++ .../dns/internal/rimuhosting/client_test.go | 303 ++++++++++++++++++ .../rimuhosting/fixtures/add_record.xml | 20 ++ .../rimuhosting/fixtures/add_record_error.xml | 2 + .../fixtures/add_record_same_domain.xml | 34 ++ .../rimuhosting/fixtures/delete_record.xml | 20 ++ .../fixtures/delete_record_error.xml | 2 + .../fixtures/delete_record_nothing.xml | 13 + .../rimuhosting/fixtures/find_records.xml | 18 ++ .../fixtures/find_records_empty.xml | 12 + .../fixtures/find_records_pattern.xml | 24 ++ providers/dns/internal/rimuhosting/model.go | 71 ++++ providers/dns/rimuhosting/rimuhosting.go | 128 ++++++++ providers/dns/rimuhosting/rimuhosting.toml | 22 ++ providers/dns/rimuhosting/rimuhosting_test.go | 120 +++++++ providers/dns/zonomi/zonomi.go | 128 ++++++++ providers/dns/zonomi/zonomi.toml | 22 ++ providers/dns/zonomi/zonomi_test.go | 120 +++++++ 23 files changed, 1415 insertions(+), 5 deletions(-) create mode 100644 docs/content/dns/zz_gen_rimuhosting.md create mode 100644 docs/content/dns/zz_gen_zonomi.md create mode 100644 providers/dns/internal/rimuhosting/client.go create mode 100644 providers/dns/internal/rimuhosting/client_test.go create mode 100644 providers/dns/internal/rimuhosting/fixtures/add_record.xml create mode 100644 providers/dns/internal/rimuhosting/fixtures/add_record_error.xml create mode 100644 providers/dns/internal/rimuhosting/fixtures/add_record_same_domain.xml create mode 100644 providers/dns/internal/rimuhosting/fixtures/delete_record.xml create mode 100644 providers/dns/internal/rimuhosting/fixtures/delete_record_error.xml create mode 100644 providers/dns/internal/rimuhosting/fixtures/delete_record_nothing.xml create mode 100644 providers/dns/internal/rimuhosting/fixtures/find_records.xml create mode 100644 providers/dns/internal/rimuhosting/fixtures/find_records_empty.xml create mode 100644 providers/dns/internal/rimuhosting/fixtures/find_records_pattern.xml create mode 100644 providers/dns/internal/rimuhosting/model.go create mode 100644 providers/dns/rimuhosting/rimuhosting.go create mode 100644 providers/dns/rimuhosting/rimuhosting.toml create mode 100644 providers/dns/rimuhosting/rimuhosting_test.go create mode 100644 providers/dns/zonomi/zonomi.go create mode 100644 providers/dns/zonomi/zonomi.toml create mode 100644 providers/dns/zonomi/zonomi_test.go diff --git a/README.md b/README.md index a9bc8c49..f9bb45a3 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,9 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [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/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) | -| [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | -| [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [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/) | -| [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [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/) | +| [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | +| [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [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/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [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/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | | | diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index cd0d2e27..0e5cfb12 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -69,6 +69,7 @@ func allDNSCodes() string { "rackspace", "regru", "rfc2136", + "rimuhosting", "route53", "sakuracloud", "scaleway", @@ -81,6 +82,7 @@ func allDNSCodes() string { "vscale", "vultr", "zoneee", + "zonomi", } sort.Strings(providers) return strings.Join(providers, ", ") @@ -1260,6 +1262,26 @@ func displayDNSHelp(name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/rfc2136`) + case "rimuhosting": + // generated from: providers/dns/rimuhosting/rimuhosting.toml + ew.writeln(`Configuration for RimuHosting.`) + ew.writeln(`Code: 'rimuhosting'`) + ew.writeln(`Since: 'v0.3.5'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "RIMUHOSTING_API_KEY": User API key`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "RIMUHOSTING_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "RIMUHOSTING_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "RIMUHOSTING_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "RIMUHOSTING_TTL": The TTL of the TXT record used for the DNS challenge`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/rimuhosting`) + case "route53": // generated from: providers/dns/route53/route53.toml ew.writeln(`Configuration for Amazon Route 53.`) @@ -1516,6 +1538,26 @@ func displayDNSHelp(name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/zoneee`) + case "zonomi": + // generated from: providers/dns/zonomi/zonomi.toml + ew.writeln(`Configuration for Zonomi.`) + ew.writeln(`Code: 'zonomi'`) + ew.writeln(`Since: 'v0.3.5'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "ZONOMI_API_KEY": User API key`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "ZONOMI_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "ZONOMI_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "ZONOMI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "ZONOMI_TTL": The TTL of the TXT record used for the DNS challenge`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/zonomi`) + case "manual": ew.writeln(`Solving the DNS-01 challenge using CLI prompt.`) default: diff --git a/docs/content/dns/zz_gen_rimuhosting.md b/docs/content/dns/zz_gen_rimuhosting.md new file mode 100644 index 00000000..2c6dd2f8 --- /dev/null +++ b/docs/content/dns/zz_gen_rimuhosting.md @@ -0,0 +1,62 @@ +--- +title: "RimuHosting" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: rimuhosting +--- + + + + + +Since: v0.3.5 + +Configuration for [RimuHosting](https://rimuhosting.com). + + + + +- Code: `rimuhosting` + +Here is an example bash command using the RimuHosting provider: + +```bash +RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ +lego --dns rimuhosting --domains my.domain.com --email my@email.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `RIMUHOSTING_API_KEY` | User API key | + +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 | +|--------------------------------|-------------| +| `RIMUHOSTING_HTTP_TIMEOUT` | API request timeout | +| `RIMUHOSTING_POLLING_INTERVAL` | Time between DNS propagation check | +| `RIMUHOSTING_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `RIMUHOSTING_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://rimuhosting.com/dns/dyndns.jsp) + + + + diff --git a/docs/content/dns/zz_gen_zonomi.md b/docs/content/dns/zz_gen_zonomi.md new file mode 100644 index 00000000..079c6da6 --- /dev/null +++ b/docs/content/dns/zz_gen_zonomi.md @@ -0,0 +1,62 @@ +--- +title: "Zonomi" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: zonomi +--- + + + + + +Since: v0.3.5 + +Configuration for [Zonomi](https://zonomi.com). + + + + +- Code: `zonomi` + +Here is an example bash command using the Zonomi provider: + +```bash +ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ +lego --dns zonomi --domains my.domain.com --email my@email.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `ZONOMI_API_KEY` | User API key | + +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 | +|--------------------------------|-------------| +| `ZONOMI_HTTP_TIMEOUT` | API request timeout | +| `ZONOMI_POLLING_INTERVAL` | Time between DNS propagation check | +| `ZONOMI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `ZONOMI_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://zonomi.com/app/dns/dyndns.jsp) + + + + diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index adf54d7b..fb87c68e 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -60,6 +60,7 @@ import ( "github.com/go-acme/lego/v3/providers/dns/rackspace" "github.com/go-acme/lego/v3/providers/dns/regru" "github.com/go-acme/lego/v3/providers/dns/rfc2136" + "github.com/go-acme/lego/v3/providers/dns/rimuhosting" "github.com/go-acme/lego/v3/providers/dns/route53" "github.com/go-acme/lego/v3/providers/dns/sakuracloud" "github.com/go-acme/lego/v3/providers/dns/scaleway" @@ -72,6 +73,7 @@ import ( "github.com/go-acme/lego/v3/providers/dns/vscale" "github.com/go-acme/lego/v3/providers/dns/vultr" "github.com/go-acme/lego/v3/providers/dns/zoneee" + "github.com/go-acme/lego/v3/providers/dns/zonomi" ) // NewDNSChallengeProviderByName Factory for DNS providers @@ -187,10 +189,12 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return rackspace.NewDNSProvider() case "regru": return regru.NewDNSProvider() - case "route53": - return route53.NewDNSProvider() case "rfc2136": return rfc2136.NewDNSProvider() + case "rimuhosting": + return rimuhosting.NewDNSProvider() + case "route53": + return route53.NewDNSProvider() case "sakuracloud": return sakuracloud.NewDNSProvider() case "scaleway": @@ -213,6 +217,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return vscale.NewDNSProvider() case "zoneee": return zoneee.NewDNSProvider() + case "zonomi": + return zonomi.NewDNSProvider() default: return nil, fmt.Errorf("unrecognized DNS provider: %s", name) } diff --git a/providers/dns/internal/rimuhosting/client.go b/providers/dns/internal/rimuhosting/client.go new file mode 100644 index 00000000..407dc6d9 --- /dev/null +++ b/providers/dns/internal/rimuhosting/client.go @@ -0,0 +1,178 @@ +package rimuhosting + +import ( + "encoding/xml" + "errors" + "io/ioutil" + "net/http" + "net/url" + "regexp" + + querystring "github.com/google/go-querystring/query" +) + +// Base URL for the RimuHosting DNS services. +const ( + DefaultZonomiBaseURL = "https://zonomi.com/app/dns/dyndns.jsp" + DefaultRimuHostingBaseURL = "https://rimuhosting.com/app/dns/dyndns.jsp" +) + +// Action names. +const ( + SetAction = "SET" + QueryAction = "QUERY" + DeleteAction = "DELETE" +) + +// Client the RimuHosting/Zonomi client. +type Client struct { + apiKey string + + HTTPClient *http.Client + BaseURL string +} + +// NewClient Creates a RimuHosting/Zonomi client. +func NewClient(apiKey string) *Client { + return &Client{ + HTTPClient: http.DefaultClient, + BaseURL: DefaultZonomiBaseURL, + apiKey: apiKey, + } +} + +// FindTXTRecords Finds TXT records. +// ex: +// - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=example.com&api_key=apikeyvaluehere +// - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=**.example.com&api_key=apikeyvaluehere +func (c Client) FindTXTRecords(domain string) ([]Record, error) { + action := ActionParameter{ + Action: QueryAction, + Name: domain, + Type: "TXT", + } + + resp, err := c.DoActions(action) + if err != nil { + return nil, err + } + + return resp.Actions.Action.Records, nil +} + +// DoActions performs actions. +func (c Client) DoActions(actions ...ActionParameter) (*DNSAPIResult, error) { + if len(actions) == 0 { + return nil, errors.New("no action") + } + + resp := &DNSAPIResult{} + + if len(actions) == 1 { + action := actionParameter{ + ActionParameter: actions[0], + APIKey: c.apiKey, + } + + err := c.do(action, resp) + if err != nil { + return nil, err + } + return resp, nil + } + + multi := c.toMultiParameters(actions) + err := c.do(multi, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +func (c Client) toMultiParameters(params []ActionParameter) multiActionParameter { + multi := multiActionParameter{ + APIKey: c.apiKey, + } + + for _, parameters := range params { + multi.Action = append(multi.Action, parameters.Action) + multi.Name = append(multi.Name, parameters.Name) + multi.Type = append(multi.Type, parameters.Type) + multi.Value = append(multi.Value, parameters.Value) + multi.TTL = append(multi.TTL, parameters.TTL) + } + + return multi +} + +func (c Client) do(params interface{}, data interface{}) error { + baseURL, err := url.Parse(c.BaseURL) + if err != nil { + return err + } + + v, err := querystring.Values(params) + if err != nil { + return err + } + + exp := regexp.MustCompile(`(%5B)(%5D)(\d+)=`) + + baseURL.RawQuery = exp.ReplaceAllString(v.Encode(), "${1}${3}${2}=") + + req, err := http.NewRequest(http.MethodGet, baseURL.String(), nil) + if err != nil { + return err + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return err + } + + defer func() { _ = resp.Body.Close() }() + + all, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode/100 != 2 { + r := APIError{} + err = xml.Unmarshal(all, &r) + if err != nil { + return err + } + return r + } + + if data != nil { + err := xml.Unmarshal(all, data) + if err != nil { + return err + } + } + + return nil +} + +// AddRecord helper to create an action to add a TXT record. +func AddRecord(domain string, content string, ttl int) ActionParameter { + return ActionParameter{ + Action: SetAction, + Name: domain, + Type: "TXT", + Value: content, + TTL: ttl, + } +} + +// DeleteRecord helper to create an action to delete a TXT record. +func DeleteRecord(domain string, content string) ActionParameter { + return ActionParameter{ + Action: DeleteAction, + Name: domain, + Type: "TXT", + Value: content, + } +} diff --git a/providers/dns/internal/rimuhosting/client_test.go b/providers/dns/internal/rimuhosting/client_test.go new file mode 100644 index 00000000..d2f57aea --- /dev/null +++ b/providers/dns/internal/rimuhosting/client_test.go @@ -0,0 +1,303 @@ +package rimuhosting + +import ( + "encoding/xml" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClient_FindTXTRecords(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + query := req.URL.Query() + + var fixture string + switch query.Get("name") { + case "example.com": + fixture = "./fixtures/find_records.xml" + case "**.example.com": + fixture = "./fixtures/find_records_pattern.xml" + default: + fixture = "./fixtures/find_records_empty.xml" + } + + err := writeResponse(rw, fixture) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client := NewClient("apikeyvaluehere") + client.BaseURL = server.URL + + testCases := []struct { + desc string + domain string + expected []Record + }{ + { + desc: "simple", + domain: "example.com", + expected: []Record{ + { + Name: "example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "3600 seconds", + Priority: "0", + }, + }, + }, + { + desc: "pattern", + domain: "**.example.com", + expected: []Record{ + { + Name: "_test.example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "3600 seconds", + Priority: "0", + }, + { + Name: "example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "3600 seconds", + Priority: "0", + }, + }, + }, + { + desc: "empty", + domain: "empty.com", + expected: nil, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + records, err := client.FindTXTRecords(test.domain) + require.NoError(t, err) + + assert.Equal(t, test.expected, records) + }) + } +} + +func TestClient_DoActions(t *testing.T) { + type expected struct { + Query string + Resp *DNSAPIResult + Error string + } + + testCases := []struct { + desc string + actions []ActionParameter + fixture string + expected expected + }{ + { + desc: "SET error", + actions: []ActionParameter{ + AddRecord("example.com", "txttxtx", 0), + }, + fixture: "./fixtures/add_record_error.xml", + expected: expected{ + Query: "action=SET&api_key=apikeyvaluehere&name=example.com&type=TXT&value=txttxtx", + Error: "ERROR: No zone found for example.com", + }, + }, + { + desc: "SET simple", + actions: []ActionParameter{ + AddRecord("example.org", "txttxtx", 0), + }, + fixture: "./fixtures/add_record.xml", + expected: expected{ + Query: "action=SET&api_key=apikeyvaluehere&name=example.org&type=TXT&value=txttxtx", + Resp: &DNSAPIResult{ + XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, + IsOk: "OK:", + ResultCounts: ResultCounts{Added: "1", Changed: "0", Unchanged: "0", Deleted: "0"}, + Actions: Actions{ + Action: Action{ + Action: "SET", + Host: "example.org", + Type: "TXT", + Records: []Record{{ + Name: "example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "3600 seconds", + Priority: "0", + }}, + }}, + }, + }, + }, + { + desc: "SET multiple values", + actions: []ActionParameter{ + AddRecord("example.org", "txttxtx", 0), + AddRecord("example.org", "sample", 0), + }, + fixture: "./fixtures/add_record_same_domain.xml", + expected: expected{ + Query: "action[0]=SET&action[1]=SET&api_key=apikeyvaluehere&name[0]=example.org&name[1]=example.org&ttl[0]=0&ttl[1]=0&type[0]=TXT&type[1]=TXT&value[0]=txttxtx&value[1]=sample", + Resp: &DNSAPIResult{ + XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, + IsOk: "OK:", + ResultCounts: ResultCounts{Added: "2", Changed: "0", Unchanged: "0", Deleted: "0"}, + Actions: Actions{ + Action: Action{ + Action: "SET", + Host: "example.org", + Type: "TXT", + Records: []Record{ + { + Name: "example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "0 seconds", + Priority: "0", + }, + { + Name: "example.org", + Type: "TXT", + Content: "sample", + TTL: "0 seconds", + Priority: "0", + }, + }, + }}}, + }, + }, + { + desc: "DELETE error", + actions: []ActionParameter{ + DeleteRecord("example.com", "txttxtx"), + }, + fixture: "./fixtures/delete_record_error.xml", + expected: expected{ + Query: "action=DELETE&api_key=apikeyvaluehere&name=example.com&type=TXT&value=txttxtx", + Error: "ERROR: No zone found for example.com", + }, + }, + { + desc: "DELETE nothing", + actions: []ActionParameter{ + DeleteRecord("example.org", "nothing"), + }, + fixture: "./fixtures/delete_record_nothing.xml", + expected: expected{ + Query: "action=DELETE&api_key=apikeyvaluehere&name=example.org&type=TXT&value=nothing", + Resp: &DNSAPIResult{ + XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, + IsOk: "OK:", + ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "0"}, + Actions: Actions{ + Action: Action{ + Action: "DELETE", + Host: "example.org", + Type: "TXT", + Records: nil, + }}}, + }, + }, + { + desc: "DELETE simple", + actions: []ActionParameter{ + DeleteRecord("example.org", "txttxtx"), + }, + fixture: "./fixtures/delete_record.xml", + expected: expected{ + Query: "action=DELETE&api_key=apikeyvaluehere&name=example.org&type=TXT&value=txttxtx", + Resp: &DNSAPIResult{ + XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, + IsOk: "OK:", + ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "1"}, + Actions: Actions{ + Action: Action{ + Action: "DELETE", + Host: "example.org", + Type: "TXT", + Records: []Record{{ + Name: "example.org", + Type: "TXT", + Content: "txttxtx", + TTL: "3600 seconds", + Priority: "0", + }}, + }}}, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + query, err := url.QueryUnescape(req.URL.RawQuery) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + if test.expected.Query != query { + http.Error(rw, fmt.Sprintf("invalid query: %s", query), http.StatusBadRequest) + return + } + + if test.expected.Error != "" { + rw.WriteHeader(http.StatusInternalServerError) + } + + err = writeResponse(rw, test.fixture) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client := NewClient("apikeyvaluehere") + client.BaseURL = server.URL + + resp, err := client.DoActions(test.actions...) + if test.expected.Error != "" { + require.EqualError(t, err, test.expected.Error) + return + } + + require.NoError(t, err) + + assert.Equal(t, test.expected.Resp, resp) + }) + } +} + +func writeResponse(rw io.Writer, filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(rw, file) + return err +} diff --git a/providers/dns/internal/rimuhosting/fixtures/add_record.xml b/providers/dns/internal/rimuhosting/fixtures/add_record.xml new file mode 100644 index 00000000..5b44a59a --- /dev/null +++ b/providers/dns/internal/rimuhosting/fixtures/add_record.xml @@ -0,0 +1,20 @@ +]>OK: + + + + + \ No newline at end of file diff --git a/providers/dns/internal/rimuhosting/fixtures/add_record_error.xml b/providers/dns/internal/rimuhosting/fixtures/add_record_error.xml new file mode 100644 index 00000000..c42693ed --- /dev/null +++ b/providers/dns/internal/rimuhosting/fixtures/add_record_error.xml @@ -0,0 +1,2 @@ +]> +ERROR: No zone found for example.com \ No newline at end of file diff --git a/providers/dns/internal/rimuhosting/fixtures/add_record_same_domain.xml b/providers/dns/internal/rimuhosting/fixtures/add_record_same_domain.xml new file mode 100644 index 00000000..7c7b755b --- /dev/null +++ b/providers/dns/internal/rimuhosting/fixtures/add_record_same_domain.xml @@ -0,0 +1,34 @@ +]>OK: + + + + + + + \ No newline at end of file diff --git a/providers/dns/internal/rimuhosting/fixtures/delete_record.xml b/providers/dns/internal/rimuhosting/fixtures/delete_record.xml new file mode 100644 index 00000000..e10d2c44 --- /dev/null +++ b/providers/dns/internal/rimuhosting/fixtures/delete_record.xml @@ -0,0 +1,20 @@ +]>OK: + + + + + \ No newline at end of file diff --git a/providers/dns/internal/rimuhosting/fixtures/delete_record_error.xml b/providers/dns/internal/rimuhosting/fixtures/delete_record_error.xml new file mode 100644 index 00000000..c42693ed --- /dev/null +++ b/providers/dns/internal/rimuhosting/fixtures/delete_record_error.xml @@ -0,0 +1,2 @@ +]> +ERROR: No zone found for example.com \ No newline at end of file diff --git a/providers/dns/internal/rimuhosting/fixtures/delete_record_nothing.xml b/providers/dns/internal/rimuhosting/fixtures/delete_record_nothing.xml new file mode 100644 index 00000000..a74320e1 --- /dev/null +++ b/providers/dns/internal/rimuhosting/fixtures/delete_record_nothing.xml @@ -0,0 +1,13 @@ +]>OK: + + + + \ No newline at end of file diff --git a/providers/dns/internal/rimuhosting/fixtures/find_records.xml b/providers/dns/internal/rimuhosting/fixtures/find_records.xml new file mode 100644 index 00000000..6c1bc4d9 --- /dev/null +++ b/providers/dns/internal/rimuhosting/fixtures/find_records.xml @@ -0,0 +1,18 @@ +]>OK: + + + + + \ No newline at end of file diff --git a/providers/dns/internal/rimuhosting/fixtures/find_records_empty.xml b/providers/dns/internal/rimuhosting/fixtures/find_records_empty.xml new file mode 100644 index 00000000..e1682440 --- /dev/null +++ b/providers/dns/internal/rimuhosting/fixtures/find_records_empty.xml @@ -0,0 +1,12 @@ +]>OK: + + + + diff --git a/providers/dns/internal/rimuhosting/fixtures/find_records_pattern.xml b/providers/dns/internal/rimuhosting/fixtures/find_records_pattern.xml new file mode 100644 index 00000000..f27d2c39 --- /dev/null +++ b/providers/dns/internal/rimuhosting/fixtures/find_records_pattern.xml @@ -0,0 +1,24 @@ +]>OK: + + + + + + \ No newline at end of file diff --git a/providers/dns/internal/rimuhosting/model.go b/providers/dns/internal/rimuhosting/model.go new file mode 100644 index 00000000..bdb33303 --- /dev/null +++ b/providers/dns/internal/rimuhosting/model.go @@ -0,0 +1,71 @@ +package rimuhosting + +import "encoding/xml" + +type ActionParameter struct { + Action string `url:"action,omitempty"` + Name string `url:"name,omitempty"` + Type string `url:"type,omitempty"` + Value string `url:"value,omitempty"` + TTL int `url:"ttl,omitempty"` + Priority int `url:"prio,omitempty"` +} + +type actionParameter struct { + ActionParameter + + APIKey string `url:"api_key,omitempty"` +} + +type multiActionParameter struct { + APIKey string `url:"api_key,omitempty"` + + Action []string `url:"action,brackets,numbered,omitempty"` + Name []string `url:"name,brackets,numbered,omitempty"` + Type []string `url:"type,brackets,numbered,omitempty"` + Value []string `url:"value,brackets,numbered,omitempty"` + TTL []int `url:"ttl,brackets,numbered,omitempty"` + Priority []int `url:"prio,brackets,numbered,omitempty"` +} + +type APIError struct { + XMLName xml.Name `xml:"error"` + Text string `xml:",chardata"` +} + +func (a APIError) Error() string { + return a.Text +} + +type DNSAPIResult struct { + XMLName xml.Name `xml:"dnsapi_result"` + IsOk string `xml:"is_ok"` + ResultCounts ResultCounts `xml:"result_counts"` + Actions Actions `xml:"actions"` +} + +type ResultCounts struct { + Added string `xml:"added,attr"` + Changed string `xml:"changed,attr"` + Unchanged string `xml:"unchanged,attr"` + Deleted string `xml:"deleted,attr"` +} + +type Actions struct { + Action Action `xml:"action"` +} + +type Action struct { + Action string `xml:"action,attr"` + Host string `xml:"host,attr"` + Type string `xml:"type,attr"` + Records []Record `xml:"record"` +} + +type Record struct { + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + Content string `xml:"content,attr"` + TTL string `xml:"ttl,attr"` + Priority string `xml:"prio,attr"` +} diff --git a/providers/dns/rimuhosting/rimuhosting.go b/providers/dns/rimuhosting/rimuhosting.go new file mode 100644 index 00000000..1ff56b5c --- /dev/null +++ b/providers/dns/rimuhosting/rimuhosting.go @@ -0,0 +1,128 @@ +// Package rimuhosting implements a DNS provider for solving the DNS-01 challenge using RimuHosting DNS. +package rimuhosting + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v3/challenge/dns01" + "github.com/go-acme/lego/v3/platform/config/env" + "github.com/go-acme/lego/v3/providers/dns/internal/rimuhosting" +) + +// Environment variables names. +const ( + envNamespace = "RIMUHOSTING_" + + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider +type Config struct { + APIKey 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{ + TTL: env.GetOrDefaultInt(EnvTTL, 3600), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider is an implementation of the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *rimuhosting.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for RimuHosting. +// Credentials must be passed in the environment variables. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("rimuhosting: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for RimuHosting. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("rimuhosting: the configuration of the DNS provider is nil") + } + + if config.APIKey == "" { + return nil, errors.New("rimuhosting: incomplete credentials, missing API key") + } + + client := rimuhosting.NewClient(config.APIKey) + client.BaseURL = rimuhosting.DefaultRimuHostingBaseURL + + return &DNSProvider{config: config, client: client}, 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 +} + +// Present creates a TXT record using the specified parameters +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + + records, err := d.client.FindTXTRecords(dns01.UnFqdn(fqdn)) + if err != nil { + return fmt.Errorf("rimuhosting: failed to find record(s) for %s: %w", domain, err) + } + + actions := []rimuhosting.ActionParameter{ + rimuhosting.AddRecord(dns01.UnFqdn(fqdn), value, d.config.TTL), + } + + for _, record := range records { + actions = append(actions, rimuhosting.AddRecord(record.Name, record.Content, d.config.TTL)) + } + + _, err = d.client.DoActions(actions...) + if err != nil { + return fmt.Errorf("rimuhosting: failed to add record(s) for %s: %w", domain, err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + + action := rimuhosting.DeleteRecord(dns01.UnFqdn(fqdn), value) + + _, err := d.client.DoActions(action) + if err != nil { + return fmt.Errorf("rimuhosting: failed to delete record for %s: %w", domain, err) + } + + return nil +} diff --git a/providers/dns/rimuhosting/rimuhosting.toml b/providers/dns/rimuhosting/rimuhosting.toml new file mode 100644 index 00000000..b91a5d3b --- /dev/null +++ b/providers/dns/rimuhosting/rimuhosting.toml @@ -0,0 +1,22 @@ +Name = "RimuHosting" +Description = '''''' +URL = "https://rimuhosting.com" +Code = "rimuhosting" +Since = "v0.3.5" + +Example = ''' +RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ +lego --dns rimuhosting --domains my.domain.com --email my@email.com run +''' + +[Configuration] + [Configuration.Credentials] + RIMUHOSTING_API_KEY = "User API key" + [Configuration.Additional] + RIMUHOSTING_POLLING_INTERVAL = "Time between DNS propagation check" + RIMUHOSTING_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + RIMUHOSTING_TTL = "The TTL of the TXT record used for the DNS challenge" + RIMUHOSTING_HTTP_TIMEOUT = "API request timeout" + +[Links] + API = "https://rimuhosting.com/dns/dyndns.jsp" diff --git a/providers/dns/rimuhosting/rimuhosting_test.go b/providers/dns/rimuhosting/rimuhosting_test.go new file mode 100644 index 00000000..e911786a --- /dev/null +++ b/providers/dns/rimuhosting/rimuhosting_test.go @@ -0,0 +1,120 @@ +package rimuhosting + +import ( + "testing" + "time" + + "github.com/go-acme/lego/v3/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIKey: "123", + }, + }, + { + desc: "missing api key", + envVars: map[string]string{ + EnvAPIKey: "", + }, + expected: "rimuhosting: some credentials information are missing: RIMUHOSTING_API_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 + expected string + apiKey string + secretKey string + }{ + { + desc: "success", + apiKey: "api_key", + secretKey: "api_secret", + }, + { + desc: "missing api key", + apiKey: "", + secretKey: "api_secret", + expected: "rimuhosting: incomplete credentials, missing API key", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + + 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) +} diff --git a/providers/dns/zonomi/zonomi.go b/providers/dns/zonomi/zonomi.go new file mode 100644 index 00000000..cc8c6999 --- /dev/null +++ b/providers/dns/zonomi/zonomi.go @@ -0,0 +1,128 @@ +// Package zonomi implements a DNS provider for solving the DNS-01 challenge using Zonomi DNS. +package zonomi + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v3/challenge/dns01" + "github.com/go-acme/lego/v3/platform/config/env" + "github.com/go-acme/lego/v3/providers/dns/internal/rimuhosting" +) + +// Environment variables names. +const ( + envNamespace = "ZONOMI_" + + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider +type Config struct { + APIKey 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{ + TTL: env.GetOrDefaultInt(EnvTTL, 3600), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider is an implementation of the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *rimuhosting.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Zonomi. +// Credentials must be passed in the environment variables. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("zonomi: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Zonomi. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("zonomi: the configuration of the DNS provider is nil") + } + + if config.APIKey == "" { + return nil, errors.New("zonomi: incomplete credentials, missing API key") + } + + client := rimuhosting.NewClient(config.APIKey) + client.BaseURL = rimuhosting.DefaultZonomiBaseURL + + return &DNSProvider{config: config, client: client}, 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 +} + +// Present creates a TXT record using the specified parameters +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + + records, err := d.client.FindTXTRecords(dns01.UnFqdn(fqdn)) + if err != nil { + return fmt.Errorf("zonomi: failed to find record(s) for %s: %w", domain, err) + } + + actions := []rimuhosting.ActionParameter{ + rimuhosting.AddRecord(dns01.UnFqdn(fqdn), value, d.config.TTL), + } + + for _, record := range records { + actions = append(actions, rimuhosting.AddRecord(record.Name, record.Content, d.config.TTL)) + } + + _, err = d.client.DoActions(actions...) + if err != nil { + return fmt.Errorf("zonomi: failed to add record(s) for %s: %w", domain, err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + + action := rimuhosting.DeleteRecord(dns01.UnFqdn(fqdn), value) + + _, err := d.client.DoActions(action) + if err != nil { + return fmt.Errorf("zonomi: failed to delete record for %s: %w", domain, err) + } + + return nil +} diff --git a/providers/dns/zonomi/zonomi.toml b/providers/dns/zonomi/zonomi.toml new file mode 100644 index 00000000..8d179341 --- /dev/null +++ b/providers/dns/zonomi/zonomi.toml @@ -0,0 +1,22 @@ +Name = "Zonomi" +Description = '''''' +URL = "https://zonomi.com" +Code = "zonomi" +Since = "v0.3.5" + +Example = ''' +ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ +lego --dns zonomi --domains my.domain.com --email my@email.com run +''' + +[Configuration] + [Configuration.Credentials] + ZONOMI_API_KEY = "User API key" + [Configuration.Additional] + ZONOMI_POLLING_INTERVAL = "Time between DNS propagation check" + ZONOMI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + ZONOMI_TTL = "The TTL of the TXT record used for the DNS challenge" + ZONOMI_HTTP_TIMEOUT = "API request timeout" + +[Links] + API = "https://zonomi.com/app/dns/dyndns.jsp" diff --git a/providers/dns/zonomi/zonomi_test.go b/providers/dns/zonomi/zonomi_test.go new file mode 100644 index 00000000..a77c853e --- /dev/null +++ b/providers/dns/zonomi/zonomi_test.go @@ -0,0 +1,120 @@ +package zonomi + +import ( + "testing" + "time" + + "github.com/go-acme/lego/v3/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIKey: "123", + }, + }, + { + desc: "missing api key", + envVars: map[string]string{ + EnvAPIKey: "", + }, + expected: "zonomi: some credentials information are missing: ZONOMI_API_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 + expected string + apiKey string + secretKey string + }{ + { + desc: "success", + apiKey: "api_key", + secretKey: "api_secret", + }, + { + desc: "missing api key", + apiKey: "", + secretKey: "api_secret", + expected: "zonomi: incomplete credentials, missing API key", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + + 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) +}