From 55968bcd2146787f6e97f9d30b130ae6a91ced35 Mon Sep 17 00:00:00 2001 From: Brett Vickers Date: Mon, 14 Mar 2016 14:28:40 -0700 Subject: [PATCH] Add DNS challenge provider for namecheap. --- CHANGELOG.md | 4 +- README.md | 1 + cli.go | 1 + cli_handlers.go | 5 +- providers/dns/namecheap/namecheap.go | 409 ++++++++++++++++++++++ providers/dns/namecheap/namecheap_test.go | 402 +++++++++++++++++++++ 6 files changed, 819 insertions(+), 3 deletions(-) create mode 100644 providers/dns/namecheap/namecheap.go create mode 100644 providers/dns/namecheap/namecheap_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 204d2e04..a2e40092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,12 @@ ## Unreleased ### Added: -- CLI: The `--dns` switch. To include the DNS challenge for consideration. Supported are the following solvers: cloudflare, digitalocean, dnsimple, gandi, route53, rfc2136 and manual. +- CLI: The `--dns` switch. To include the DNS challenge for consideration. Supported are the following solvers: cloudflare, digitalocean, dnsimple, gandi, namecheap, route53, rfc2136 and manual. - CLI: The `--accept-tos` switch. Indicates your acceptance of the Let's Encrypt terms of service without prompting you. - lib: A new type for challenge identifiers `Challenge` - lib: A new interface for custom challenge providers `ChallengeProvider` - lib: SetChallengeProvider function. Pass a challenge identifier and a Provider to replace the default behaviour of a challenge. -- lib: The DNS-01 challenge has been implemented with modular solvers using the `ChallengeProvider` interface. Included solvers are: cloudflare, digitalocean, dnsimple, gandi, route53, rfc2136 and manual. +- lib: The DNS-01 challenge has been implemented with modular solvers using the `ChallengeProvider` interface. Included solvers are: cloudflare, digitalocean, dnsimple, gandi, namecheap, route53, rfc2136 and manual. ### Changed - lib: ExcludeChallenges now expects to be passed an array of `Challenge` types. diff --git a/README.md b/README.md index 500f8fed..73acc99f 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ GLOBAL OPTIONS: digitalocean: DO_AUTH_TOKEN dnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY gandi: GANDI_API_KEY + namecheap: NAMECHEAP_API_USER, NAMECHEAP_API_KEY route53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION rfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER manual: none diff --git a/cli.go b/cli.go index b61c0508..2077bce5 100644 --- a/cli.go +++ b/cli.go @@ -138,6 +138,7 @@ func main() { "\n\tdigitalocean: DO_AUTH_TOKEN" + "\n\tdnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY" + "\n\tgandi: GANDI_API_KEY" + + "\n\tnamecheap: NAMECHEAP_API_USER, NAMECHEAP_API_KEY" + "\n\troute53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION" + "\n\trfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER" + "\n\tmanual: none", diff --git a/cli_handlers.go b/cli_handlers.go index 2e025c56..eee7508d 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -15,6 +15,7 @@ import ( "github.com/xenolf/lego/providers/dns/digitalocean" "github.com/xenolf/lego/providers/dns/dnsimple" "github.com/xenolf/lego/providers/dns/gandi" + "github.com/xenolf/lego/providers/dns/namecheap" "github.com/xenolf/lego/providers/dns/rfc2136" "github.com/xenolf/lego/providers/dns/route53" "github.com/xenolf/lego/providers/http/webroot" @@ -62,7 +63,7 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { } client.SetChallengeProvider(acme.HTTP01, provider) - + // --webroot=foo indicates that the user specifically want to do a HTTP challenge // infer that the user also wants to exclude all other challenges client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) @@ -96,6 +97,8 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { case "gandi": apiKey := os.Getenv("GANDI_API_KEY") provider, err = gandi.NewDNSProvider(apiKey) + case "namecheap": + provider, err = namecheap.NewDNSProvider("", "") case "route53": awsRegion := os.Getenv("AWS_REGION") provider, err = route53.NewDNSProvider("", "", awsRegion) diff --git a/providers/dns/namecheap/namecheap.go b/providers/dns/namecheap/namecheap.go new file mode 100644 index 00000000..55f8a3d1 --- /dev/null +++ b/providers/dns/namecheap/namecheap.go @@ -0,0 +1,409 @@ +// Package namecheap implements a DNS provider for solving the DNS-01 +// challenge using namecheap DNS. +package namecheap + +import ( + "encoding/xml" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/xenolf/lego/acme" +) + +// Notes about namecheap's tool API: +// 1. Using the API requires registration. Once registered, use your account +// name and API key to access the API. +// 2. There is no API to add or modify a single DNS record. Instead you must +// read the entire list of records, make modifications, and then write the +// entire updated list of records. (Yuck.) +// 3. Namecheap's DNS updates can be slow to propagate. I've seen them take +// as long as an hour. +// 4. Namecheap requires you to whitelist the IP address from which you call +// its APIs. It also requires all API calls to include the whitelisted IP +// address as a form or query string value. This code uses a namecheap +// service to query the client's IP address. + +var ( + debug = false + defaultBaseURL = "https://api.namecheap.com/xml.response" + getIpURL = "https://dynamicdns.park-your-domain.com/getip" +) + +// DNSProvider is an implementation of the ChallengeProviderTimeout interface +// that uses Namecheap's tool API to manage TXT records for a domain. +type DNSProvider struct { + baseURL string + apiUser string + apiKey string + clientIP string +} + +// NewDNSProvider returns a new DNSProvider instance. apiUser is the namecheap +// API user's account name, and apiKey is the account's API access key. +func NewDNSProvider(apiUser, apiKey string) (*DNSProvider, error) { + if apiUser == "" || apiKey == "" { + apiUser = os.Getenv("NAMECHEAP_API_USER") + apiKey = os.Getenv("NAMECHEAP_API_KEY") + if apiUser == "" || apiKey == "" { + return nil, fmt.Errorf("Namecheap credentials missing") + } + } + + clientIP, err := getClientIP() + if err != nil { + return nil, err + } + + return &DNSProvider{ + baseURL: defaultBaseURL, + apiUser: apiUser, + apiKey: apiKey, + clientIP: clientIP, + }, nil +} + +// Namecheap can sometimes take a long time to complete an update, so wait +// up to 60 minutes for the update to propagate. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return 60 * time.Minute, 15 * time.Second +} + +// host describes a DNS record returned by the Namecheap DNS gethosts API. +// Namecheap uses the term "host" to refer to all DNS records that include +// a host field (A, AAAA, CNAME, NS, TXT, URL). +type host struct { + Type string `xml:",attr"` + Name string `xml:",attr"` + Address string `xml:",attr"` + MXPref string `xml:",attr"` + TTL string `xml:",attr"` +} + +// apierror describes an error record in a namecheap API response. +type apierror struct { + Number int `xml:",attr"` + Description string `xml:",innerxml"` +} + +// getClientIP returns the client's public IP address. It uses namecheap's +// IP discovery service to perform the lookup. +func getClientIP() (addr string, err error) { + resp, err := http.Get(getIpURL) + if err != nil { + return "", err + } + defer resp.Body.Close() + + clientIP, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if debug { + fmt.Println("Client IP:", string(clientIP)) + } + return string(clientIP), nil +} + +// A challenge repesents all the data needed to specify a dns-01 challenge +// to lets-encrypt. +type challenge struct { + domain string + key string + keyFqdn string + keyValue string + tld string + sld string + host string +} + +// newChallenge builds a challenge record from a domain name, a challenge +// authentication key, and a map of available TLDs. +func newChallenge(domain, keyAuth string, tlds map[string]string) (*challenge, error) { + domain = acme.UnFqdn(domain) + parts := strings.Split(domain, ".") + + // Find the longest matching TLD. + longest := -1 + for i := len(parts); i > 0; i-- { + t := strings.Join(parts[i-1:], ".") + if _, found := tlds[t]; found { + longest = i - 1 + } + } + if longest < 1 { + return nil, fmt.Errorf("Invalid domain name '%s'", domain) + } + + tld := strings.Join(parts[longest:], ".") + sld := parts[longest-1] + + var host string + if longest >= 1 { + host = strings.Join(parts[:longest-1], ".") + } + + key, keyValue, _ := acme.DNS01Record(domain, keyAuth) + + return &challenge{ + domain: domain, + key: "_acme-challenge." + host, + keyFqdn: key, + keyValue: keyValue, + tld: tld, + sld: sld, + host: host, + }, nil +} + +// setGlobalParams adds the namecheap global parameters to the provided url +// Values record. +func (d *DNSProvider) setGlobalParams(v *url.Values, cmd string) { + v.Set("ApiUser", d.apiUser) + v.Set("ApiKey", d.apiKey) + v.Set("UserName", d.apiUser) + v.Set("ClientIp", d.clientIP) + v.Set("Command", cmd) +} + +// getTLDs requests the list of available TLDs from namecheap. +func (d *DNSProvider) getTLDs() (tlds map[string]string, err error) { + values := make(url.Values) + d.setGlobalParams(&values, "namecheap.domains.getTldList") + + reqURL, _ := url.Parse(d.baseURL) + reqURL.RawQuery = values.Encode() + + resp, err := http.Get(reqURL.String()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("getHosts HTTP error %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + type GetTldsResponse struct { + XMLName xml.Name `xml:"ApiResponse"` + Errors []apierror `xml:"Errors>Error"` + Result []struct { + Name string `xml:",attr"` + } `xml:"CommandResponse>Tlds>Tld"` + } + + var gtr GetTldsResponse + if err := xml.Unmarshal(body, >r); err != nil { + return nil, err + } + if len(gtr.Errors) > 0 { + return nil, fmt.Errorf("Namecheap error: %s [%d]", + gtr.Errors[0].Description, gtr.Errors[0].Number) + } + + tlds = make(map[string]string) + for _, t := range gtr.Result { + tlds[t.Name] = t.Name + } + return tlds, nil +} + +// getHosts reads the full list of DNS host records using the Namecheap API. +func (d *DNSProvider) getHosts(ch *challenge) (hosts []host, err error) { + values := make(url.Values) + d.setGlobalParams(&values, "namecheap.domains.dns.getHosts") + values.Set("SLD", ch.sld) + values.Set("TLD", ch.tld) + + reqURL, _ := url.Parse(d.baseURL) + reqURL.RawQuery = values.Encode() + + resp, err := http.Get(reqURL.String()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("getHosts HTTP error %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + type GetHostsResponse struct { + XMLName xml.Name `xml:"ApiResponse"` + Status string `xml:"Status,attr"` + Errors []apierror `xml:"Errors>Error"` + Hosts []host `xml:"CommandResponse>DomainDNSGetHostsResult>host"` + } + + var ghr GetHostsResponse + if err = xml.Unmarshal(body, &ghr); err != nil { + return nil, err + } + if len(ghr.Errors) > 0 { + return nil, fmt.Errorf("Namecheap error: %s [%d]", + ghr.Errors[0].Description, ghr.Errors[0].Number) + } + + return ghr.Hosts, nil +} + +// setHosts writes the full list of DNS host records using the Namecheap API. +func (d *DNSProvider) setHosts(ch *challenge, hosts []host) error { + values := make(url.Values) + d.setGlobalParams(&values, "namecheap.domains.dns.setHosts") + values.Set("SLD", ch.sld) + values.Set("TLD", ch.tld) + + for i, h := range hosts { + ind := fmt.Sprintf("%d", i+1) + values.Add("HostName"+ind, h.Name) + values.Add("RecordType"+ind, h.Type) + values.Add("Address"+ind, h.Address) + values.Add("MXPref"+ind, h.MXPref) + values.Add("TTL"+ind, h.TTL) + } + + resp, err := http.PostForm(d.baseURL, values) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return fmt.Errorf("setHosts HTTP error %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + type SetHostsResponse struct { + XMLName xml.Name `xml:"ApiResponse"` + Status string `xml:"Status,attr"` + Errors []apierror `xml:"Errors>Error"` + Result struct { + IsSuccess string `xml:",attr"` + } `xml:"CommandResponse>DomainDNSSetHostsResult"` + } + + var shr SetHostsResponse + if err := xml.Unmarshal(body, &shr); err != nil { + return err + } + if len(shr.Errors) > 0 { + return fmt.Errorf("Namecheap error: %s [%d]", + shr.Errors[0].Description, shr.Errors[0].Number) + } + if shr.Result.IsSuccess != "true" { + return fmt.Errorf("Namecheap setHosts failed.") + } + + return nil +} + +// addChallengeRecord adds a DNS challenge TXT record to a list of namecheap +// host records. +func (d *DNSProvider) addChallengeRecord(ch *challenge, hosts *[]host) { + host := host{ + Name: ch.key, + Type: "TXT", + Address: ch.keyValue, + MXPref: "10", + TTL: "120", + } + + // If there's already a TXT record with the same name, replace it. + for i, h := range *hosts { + if h.Name == ch.key && h.Type == "TXT" { + (*hosts)[i] = host + return + } + } + + // No record was replaced, so add a new one. + *hosts = append(*hosts, host) +} + +// removeChallengeRecord removes a DNS challenge TXT record from a list of +// namecheap host records. Return true if a record was removed. +func (d *DNSProvider) removeChallengeRecord(ch *challenge, hosts *[]host) bool { + // Find the challenge TXT record and remove it if found. + for i, h := range *hosts { + if h.Name == ch.key && h.Type == "TXT" { + *hosts = append((*hosts)[:i], (*hosts)[i+1:]...) + return true + } + } + + return false +} + +// Present installs a TXT record for the DNS challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + tlds, err := d.getTLDs() + if err != nil { + return err + } + + ch, err := newChallenge(domain, keyAuth, tlds) + if err != nil { + return err + } + + hosts, err := d.getHosts(ch) + if err != nil { + return err + } + + d.addChallengeRecord(ch, &hosts) + + if debug { + for _, h := range hosts { + fmt.Printf( + "%-5.5s %-30.30s %-6s %-70.70s\n", + h.Type, h.Name, h.TTL, h.Address) + } + } + + return d.setHosts(ch, hosts) +} + +// CleanUp removes a TXT record used for a previous DNS challenge. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + tlds, err := d.getTLDs() + if err != nil { + return err + } + + ch, err := newChallenge(domain, keyAuth, tlds) + if err != nil { + return err + } + + hosts, err := d.getHosts(ch) + if err != nil { + return err + } + + if removed := d.removeChallengeRecord(ch, &hosts); !removed { + return nil + } + + return d.setHosts(ch, hosts) +} diff --git a/providers/dns/namecheap/namecheap_test.go b/providers/dns/namecheap/namecheap_test.go new file mode 100644 index 00000000..6f24e855 --- /dev/null +++ b/providers/dns/namecheap/namecheap_test.go @@ -0,0 +1,402 @@ +package namecheap + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +var ( + fakeUser = "foo" + fakeKey = "bar" + fakeClientIP = "10.0.0.1" + + tlds = map[string]string{ + "com.au": "com.au", + "com": "com", + "co.uk": "co.uk", + "uk": "uk", + "edu": "edu", + "co.com": "co.com", + "za.com": "za.com", + } +) + +func assertEq(t *testing.T, variable, got, want string) { + if got != want { + t.Errorf("Expected %s to be '%s' but got '%s'", variable, want, got) + } +} + +func assertHdr(tc *testcase, t *testing.T, values *url.Values) { + ch, _ := newChallenge(tc.domain, "", tlds) + + assertEq(t, "ApiUser", values.Get("ApiUser"), fakeUser) + assertEq(t, "ApiKey", values.Get("ApiKey"), fakeKey) + assertEq(t, "UserName", values.Get("UserName"), fakeUser) + assertEq(t, "ClientIp", values.Get("ClientIp"), fakeClientIP) + assertEq(t, "SLD", values.Get("SLD"), ch.sld) + assertEq(t, "TLD", values.Get("TLD"), ch.tld) +} + +func mockServer(tc *testcase, t *testing.T, w http.ResponseWriter, r *http.Request) { + switch r.Method { + + case "GET": + values := r.URL.Query() + cmd := values.Get("Command") + switch cmd { + case "namecheap.domains.dns.getHosts": + assertHdr(tc, t, &values) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, tc.getHostsResponse) + case "namecheap.domains.getTldList": + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, responseGetTlds) + default: + t.Errorf("Unexpected GET command: %s", cmd) + } + + case "POST": + r.ParseForm() + values := r.Form + cmd := values.Get("Command") + switch cmd { + case "namecheap.domains.dns.setHosts": + assertHdr(tc, t, &values) + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, tc.setHostsResponse) + default: + t.Errorf("Unexpected POST command: %s", cmd) + } + + default: + t.Errorf("Unexpected http method: %s", r.Method) + + } +} + +func testGetHosts(tc *testcase, t *testing.T) { + mock := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + mockServer(tc, t, w, r) + })) + defer mock.Close() + + prov := &DNSProvider{ + baseURL: mock.URL, + apiUser: fakeUser, + apiKey: fakeKey, + clientIP: fakeClientIP, + } + + ch, _ := newChallenge(tc.domain, "", tlds) + hosts, err := prov.getHosts(ch) + if tc.errString != "" { + if err == nil || err.Error() != tc.errString { + t.Errorf("Namecheap getHosts case %s expected error", tc.name) + } + } else { + if err != nil { + t.Errorf("Namecheap getHosts case %s failed\n%v", tc.name, err) + } + } + +next1: + for _, h := range hosts { + for _, th := range tc.hosts { + if h == th { + continue next1 + } + } + t.Errorf("getHosts case %s unexpected record [%s:%s:%s]", + tc.name, h.Type, h.Name, h.Address) + } + +next2: + for _, th := range tc.hosts { + for _, h := range hosts { + if h == th { + continue next2 + } + } + t.Errorf("getHosts case %s missing record [%s:%s:%s]", + tc.name, th.Type, th.Name, th.Address) + } +} + +func mockDNSProvider(url string) *DNSProvider { + return &DNSProvider{ + baseURL: url, + apiUser: fakeUser, + apiKey: fakeKey, + clientIP: fakeClientIP, + } +} + +func testSetHosts(tc *testcase, t *testing.T) { + mock := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + mockServer(tc, t, w, r) + })) + defer mock.Close() + + prov := mockDNSProvider(mock.URL) + ch, _ := newChallenge(tc.domain, "", tlds) + hosts, err := prov.getHosts(ch) + if tc.errString != "" { + if err == nil || err.Error() != tc.errString { + t.Errorf("Namecheap getHosts case %s expected error", tc.name) + } + } else { + if err != nil { + t.Errorf("Namecheap getHosts case %s failed\n%v", tc.name, err) + } + } + if err != nil { + return + } + + err = prov.setHosts(ch, hosts) + if err != nil { + t.Errorf("Namecheap setHosts case %s failed", tc.name) + } +} + +func testPresent(tc *testcase, t *testing.T) { + mock := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + mockServer(tc, t, w, r) + })) + defer mock.Close() + + prov := mockDNSProvider(mock.URL) + err := prov.Present(tc.domain, "", "dummyKey") + if tc.errString != "" { + if err == nil || err.Error() != tc.errString { + t.Errorf("Namecheap Present case %s expected error", tc.name) + } + } else { + if err != nil { + t.Errorf("Namecheap Present case %s failed\n%v", tc.name, err) + } + } +} + +func testCleanUp(tc *testcase, t *testing.T) { + mock := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + mockServer(tc, t, w, r) + })) + defer mock.Close() + + prov := mockDNSProvider(mock.URL) + err := prov.CleanUp(tc.domain, "", "dummyKey") + if tc.errString != "" { + if err == nil || err.Error() != tc.errString { + t.Errorf("Namecheap CleanUp case %s expected error", tc.name) + } + } else { + if err != nil { + t.Errorf("Namecheap CleanUp case %s failed\n%v", tc.name, err) + } + } +} + +func TestNamecheap(t *testing.T) { + for _, tc := range testcases { + testGetHosts(&tc, t) + testSetHosts(&tc, t) + testPresent(&tc, t) + testCleanUp(&tc, t) + } +} + +func TestNamecheapDomainSplit(t *testing.T) { + tests := []struct { + domain string + valid bool + tld string + sld string + host string + }{ + {"a.b.c.test.co.uk", true, "co.uk", "test", "a.b.c"}, + {"test.co.uk", true, "co.uk", "test", ""}, + {"test.com", true, "com", "test", ""}, + {"test.co.com", true, "co.com", "test", ""}, + {"www.test.com.au", true, "com.au", "test", "www"}, + {"www.za.com", true, "za.com", "www", ""}, + {"", false, "", "", ""}, + {"a", false, "", "", ""}, + {"com", false, "", "", ""}, + {"co.com", false, "", "", ""}, + {"co.uk", false, "", "", ""}, + {"test.au", false, "", "", ""}, + {"za.com", false, "", "", ""}, + {"www.za", false, "", "", ""}, + {"www.test.au", false, "", "", ""}, + {"www.test.unk", false, "", "", ""}, + } + + for _, test := range tests { + valid := true + ch, err := newChallenge(test.domain, "", tlds) + if err != nil { + valid = false + } + + if test.valid && !valid { + t.Errorf("Expected '%s' to split", test.domain) + } else if !test.valid && valid { + t.Errorf("Expected '%s' to produce error", test.domain) + } + + if test.valid && valid { + assertEq(t, "domain", ch.domain, test.domain) + assertEq(t, "tld", ch.tld, test.tld) + assertEq(t, "sld", ch.sld, test.sld) + assertEq(t, "host", ch.host, test.host) + } + } +} + +type testcase struct { + name string + domain string + hosts []host + errString string + getHostsResponse string + setHostsResponse string +} + +var testcases []testcase = []testcase{ + { + "Test:Success:1", + "test.example.com", + []host{ + {"A", "home", "10.0.0.1", "10", "1799"}, + {"A", "www", "10.0.0.2", "10", "1200"}, + {"AAAA", "a", "::0", "10", "1799"}, + {"CNAME", "*", "example.com.", "10", "1799"}, + {"MXE", "example.com", "10.0.0.5", "10", "1800"}, + {"URL", "xyz", "https://google.com", "10", "1799"}, + }, + "", + responseGetHostsSuccess1, + responseSetHostsSuccess1, + }, + { + "Test:Success:2", + "example.com", + []host{ + {"A", "@", "10.0.0.2", "10", "1200"}, + {"A", "www", "10.0.0.3", "10", "60"}, + }, + "", + responseGetHostsSuccess2, + responseSetHostsSuccess2, + }, + { + "Test:Error:BadApiKey:1", + "test.example.com", + nil, + "Namecheap error: API Key is invalid or API access has not been enabled [1011102]", + responseGetHostsErrorBadApiKey1, + "", + }, +} + +var responseGetHostsSuccess1 = ` + + + + namecheap.domains.dns.getHosts + + + + + + + + + + + PHX01SBAPI01 + --5:00 + 3.338 +` + +var responseSetHostsSuccess1 = ` + + + + namecheap.domains.dns.setHosts + + + + + + PHX01SBAPI01 + --5:00 + 2.347 +` + +var responseGetHostsSuccess2 = ` + + + + namecheap.domains.dns.getHosts + + + + + + + PHX01SBAPI01 + --5:00 + 3.338 +` + +var responseSetHostsSuccess2 = ` + + + + namecheap.domains.dns.setHosts + + + + + + PHX01SBAPI01 + --5:00 + 2.347 +` + +var responseGetHostsErrorBadApiKey1 = ` + + + API Key is invalid or API access has not been enabled + + + + PHX01SBAPI01 + --5:00 + 0 +` + +var responseGetTlds = ` + + + + namecheap.domains.getTldList + + + Most recognized top level domain + + + PHX01SBAPI01 + --5:00 + 0.004 +`