forked from TrueCloudLab/lego
Add DNS challenge provider for namecheap.
This commit is contained in:
parent
8c3023d198
commit
55968bcd21
6 changed files with 819 additions and 3 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
1
cli.go
1
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",
|
||||
|
|
|
@ -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"
|
||||
|
@ -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)
|
||||
|
|
409
providers/dns/namecheap/namecheap.go
Normal file
409
providers/dns/namecheap/namecheap.go
Normal file
|
@ -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)
|
||||
}
|
402
providers/dns/namecheap/namecheap_test.go
Normal file
402
providers/dns/namecheap/namecheap_test.go
Normal file
|
@ -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 = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
|
||||
<Errors />
|
||||
<Warnings />
|
||||
<RequestedCommand>namecheap.domains.dns.getHosts</RequestedCommand>
|
||||
<CommandResponse Type="namecheap.domains.dns.getHosts">
|
||||
<DomainDNSGetHostsResult Domain="example.com" EmailType="MXE" IsUsingOurDNS="true">
|
||||
<host HostId="217076" Name="www" Type="A" Address="10.0.0.2" MXPref="10" TTL="1200" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
||||
<host HostId="217069" Name="home" Type="A" Address="10.0.0.1" MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
||||
<host HostId="217071" Name="a" Type="AAAA" Address="::0" MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
||||
<host HostId="217075" Name="*" Type="CNAME" Address="example.com." MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
||||
<host HostId="217073" Name="example.com" Type="MXE" Address="10.0.0.5" MXPref="10" TTL="1800" AssociatedAppTitle="MXE" FriendlyName="MXE1" IsActive="true" IsDDNSEnabled="false" />
|
||||
<host HostId="217077" Name="xyz" Type="URL" Address="https://google.com" MXPref="10" TTL="1799" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
||||
</DomainDNSGetHostsResult>
|
||||
</CommandResponse>
|
||||
<Server>PHX01SBAPI01</Server>
|
||||
<GMTTimeDifference>--5:00</GMTTimeDifference>
|
||||
<ExecutionTime>3.338</ExecutionTime>
|
||||
</ApiResponse>`
|
||||
|
||||
var responseSetHostsSuccess1 = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
|
||||
<Errors />
|
||||
<Warnings />
|
||||
<RequestedCommand>namecheap.domains.dns.setHosts</RequestedCommand>
|
||||
<CommandResponse Type="namecheap.domains.dns.setHosts">
|
||||
<DomainDNSSetHostsResult Domain="example.com" IsSuccess="true">
|
||||
<Warnings />
|
||||
</DomainDNSSetHostsResult>
|
||||
</CommandResponse>
|
||||
<Server>PHX01SBAPI01</Server>
|
||||
<GMTTimeDifference>--5:00</GMTTimeDifference>
|
||||
<ExecutionTime>2.347</ExecutionTime>
|
||||
</ApiResponse>`
|
||||
|
||||
var responseGetHostsSuccess2 = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
|
||||
<Errors />
|
||||
<Warnings />
|
||||
<RequestedCommand>namecheap.domains.dns.getHosts</RequestedCommand>
|
||||
<CommandResponse Type="namecheap.domains.dns.getHosts">
|
||||
<DomainDNSGetHostsResult Domain="example.com" EmailType="MXE" IsUsingOurDNS="true">
|
||||
<host HostId="217076" Name="@" Type="A" Address="10.0.0.2" MXPref="10" TTL="1200" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
||||
<host HostId="217069" Name="www" Type="A" Address="10.0.0.3" MXPref="10" TTL="60" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
|
||||
</DomainDNSGetHostsResult>
|
||||
</CommandResponse>
|
||||
<Server>PHX01SBAPI01</Server>
|
||||
<GMTTimeDifference>--5:00</GMTTimeDifference>
|
||||
<ExecutionTime>3.338</ExecutionTime>
|
||||
</ApiResponse>`
|
||||
|
||||
var responseSetHostsSuccess2 = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
|
||||
<Errors />
|
||||
<Warnings />
|
||||
<RequestedCommand>namecheap.domains.dns.setHosts</RequestedCommand>
|
||||
<CommandResponse Type="namecheap.domains.dns.setHosts">
|
||||
<DomainDNSSetHostsResult Domain="example.com" IsSuccess="true">
|
||||
<Warnings />
|
||||
</DomainDNSSetHostsResult>
|
||||
</CommandResponse>
|
||||
<Server>PHX01SBAPI01</Server>
|
||||
<GMTTimeDifference>--5:00</GMTTimeDifference>
|
||||
<ExecutionTime>2.347</ExecutionTime>
|
||||
</ApiResponse>`
|
||||
|
||||
var responseGetHostsErrorBadApiKey1 = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<ApiResponse Status="ERROR" xmlns="http://api.namecheap.com/xml.response">
|
||||
<Errors>
|
||||
<Error Number="1011102">API Key is invalid or API access has not been enabled</Error>
|
||||
</Errors>
|
||||
<Warnings />
|
||||
<RequestedCommand />
|
||||
<Server>PHX01SBAPI01</Server>
|
||||
<GMTTimeDifference>--5:00</GMTTimeDifference>
|
||||
<ExecutionTime>0</ExecutionTime>
|
||||
</ApiResponse>`
|
||||
|
||||
var responseGetTlds = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
|
||||
<Errors />
|
||||
<Warnings />
|
||||
<RequestedCommand>namecheap.domains.getTldList</RequestedCommand>
|
||||
<CommandResponse Type="namecheap.domains.getTldList">
|
||||
<Tlds>
|
||||
<Tld Name="com" NonRealTime="false" MinRegisterYears="1" MaxRegisterYears="10" MinRenewYears="1" MaxRenewYears="10" RenewalMinDays="0" RenewalMaxDays="4000" ReactivateMaxDays="27" MinTransferYears="1" MaxTransferYears="1" IsApiRegisterable="true" IsApiRenewable="true" IsApiTransferable="true" IsEppRequired="true" IsDisableModContact="false" IsDisableWGAllot="false" IsIncludeInExtendedSearchOnly="false" SequenceNumber="10" Type="GTLD" SubType="" IsSupportsIDN="true" Category="A" SupportsRegistrarLock="true" AddGracePeriodDays="5" WhoisVerification="false" ProviderApiDelete="true" TldState="" SearchGroup="" Registry="">Most recognized top level domain<Categories><TldCategory Name="popular" SequenceNumber="10" /></Categories></Tld>
|
||||
</Tlds>
|
||||
</CommandResponse>
|
||||
<Server>PHX01SBAPI01</Server>
|
||||
<GMTTimeDifference>--5:00</GMTTimeDifference>
|
||||
<ExecutionTime>0.004</ExecutionTime>
|
||||
</ApiResponse>`
|
Loading…
Reference in a new issue