From 91b13b10b9b5f0fa32454a4249de26936c72a201 Mon Sep 17 00:00:00 2001 From: Pat Moroney Date: Wed, 14 Mar 2018 11:43:09 -0600 Subject: [PATCH] add Name.com provider (#480) * add Name.com provider * add namedotcom provider env vars to output of cli.go --- cli.go | 1 + providers/dns/dns_providers.go | 3 + providers/dns/namedotcom/namedotcom.go | 124 ++++++++++++++++++++ providers/dns/namedotcom/namedotcom_test.go | 58 +++++++++ 4 files changed, 186 insertions(+) create mode 100644 providers/dns/namedotcom/namedotcom.go create mode 100644 providers/dns/namedotcom/namedotcom_test.go diff --git a/cli.go b/cli.go index 2f4b8407..95801173 100644 --- a/cli.go +++ b/cli.go @@ -216,6 +216,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tlightsail:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, DNS_ZONE") fmt.Fprintln(w, "\tmanual:\tnone") fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_API_USER, NAMECHEAP_API_KEY") + fmt.Fprintln(w, "\tnamedotcom:\tNAMECOM_USERNAME, NAMECOM_API_TOKEN") fmt.Fprintln(w, "\trackspace:\tRACKSPACE_USER, RACKSPACE_API_KEY") fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER") fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_HOSTED_ZONE_ID") diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index ada957cb..b53c249f 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -22,6 +22,7 @@ import ( "github.com/xenolf/lego/providers/dns/lightsail" "github.com/xenolf/lego/providers/dns/linode" "github.com/xenolf/lego/providers/dns/namecheap" + "github.com/xenolf/lego/providers/dns/namedotcom" "github.com/xenolf/lego/providers/dns/ns1" "github.com/xenolf/lego/providers/dns/otc" "github.com/xenolf/lego/providers/dns/ovh" @@ -72,6 +73,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) provider, err = acme.NewDNSProviderManual() case "namecheap": provider, err = namecheap.NewDNSProvider() + case "namedotcom": + provider, err = namedotcom.NewDNSProvider() case "rackspace": provider, err = rackspace.NewDNSProvider() case "route53": diff --git a/providers/dns/namedotcom/namedotcom.go b/providers/dns/namedotcom/namedotcom.go new file mode 100644 index 00000000..2df4a597 --- /dev/null +++ b/providers/dns/namedotcom/namedotcom.go @@ -0,0 +1,124 @@ +// Package namedotcom implements a DNS provider for solving the DNS-01 challenge +// using Name.com's DNS service. +package namedotcom + +import ( + "fmt" + "os" + "strings" + + "github.com/namedotcom/go/namecom" + "github.com/xenolf/lego/acme" +) + +// DNSProvider is an implementation of the acme.ChallengeProvider interface. +type DNSProvider struct { + client *namecom.NameCom +} + +// NewDNSProvider returns a DNSProvider instance configured for namedotcom. +// Credentials must be passed in the environment variables: NAMECOM_USERNAME and NAMECOM_API_TOKEN +func NewDNSProvider() (*DNSProvider, error) { + username := os.Getenv("NAMECOM_USERNAME") + apiToken := os.Getenv("NAMECOM_API_TOKEN") + server := os.Getenv("NAMECOM_SERVER") + + return NewDNSProviderCredentials(username, apiToken, server) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for namedotcom. +func NewDNSProviderCredentials(username, apiToken, server string) (*DNSProvider, error) { + if username == "" { + return nil, fmt.Errorf("Name.com Username is required") + } + if apiToken == "" { + return nil, fmt.Errorf("Name.com API token is required") + } + + client := namecom.New(username, apiToken) + + if server != "" { + client.Server = server + } + + return &DNSProvider{client: client}, nil +} + +// Present creates a TXT record to fulfil the dns-01 challenge. +func (c *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) + + request := &namecom.Record{ + DomainName: domain, + Host: c.extractRecordName(fqdn, domain), + Type: "TXT", + TTL: uint32(ttl), + Answer: value, + } + + _, err := c.client.CreateRecord(request) + if err != nil { + return fmt.Errorf("namedotcom API call failed: %v", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + + records, err := c.getRecords(domain) + if err != nil { + return err + } + + for _, rec := range records { + if rec.Fqdn == fqdn && rec.Type == "TXT" { + request := &namecom.DeleteRecordRequest{ + DomainName: domain, + ID: rec.ID, + } + _, err := c.client.DeleteRecord(request) + if err != nil { + return err + } + } + } + + return nil +} + +func (c *DNSProvider) getRecords(domain string) ([]*namecom.Record, error) { + var ( + err error + records []*namecom.Record + response *namecom.ListRecordsResponse + ) + + request := &namecom.ListRecordsRequest{ + DomainName: domain, + Page: 1, + } + + for request.Page > 0 { + response, err = c.client.ListRecords(request) + if err != nil { + return nil, err + } + + records = append(records, response.Records...) + request.Page = response.NextPage + } + + return records, nil +} + +func (c *DNSProvider) extractRecordName(fqdn, domain string) string { + name := acme.UnFqdn(fqdn) + if idx := strings.Index(name, "."+domain); idx != -1 { + return name[:idx] + } + return name +} diff --git a/providers/dns/namedotcom/namedotcom_test.go b/providers/dns/namedotcom/namedotcom_test.go new file mode 100644 index 00000000..6d00a464 --- /dev/null +++ b/providers/dns/namedotcom/namedotcom_test.go @@ -0,0 +1,58 @@ +package namedotcom + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + namedotcomLiveTest bool + namedotcomUsername string + namedotcomAPIToken string + namedotcomDomain string + namedotcomServer string +) + +func init() { + namedotcomUsername = os.Getenv("NAMEDOTCOM_USERNAME") + namedotcomAPIToken = os.Getenv("NAMEDOTCOM_API_TOKEN") + namedotcomDomain = os.Getenv("NAMEDOTCOM_DOMAIN") + namedotcomServer = os.Getenv("NAMEDOTCOM_SERVER") + + if len(namedotcomAPIToken) > 0 && len(namedotcomUsername) > 0 && len(namedotcomDomain) > 0 { + namedotcomLiveTest = true + } +} + +func TestLivenamedotcomPresent(t *testing.T) { + if !namedotcomLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProviderCredentials(namedotcomUsername, namedotcomAPIToken, namedotcomServer) + assert.NoError(t, err) + + err = provider.Present(namedotcomDomain, "", "123d==") + assert.NoError(t, err) +} + +// +// Cleanup +// + +func TestLivenamedotcomCleanUp(t *testing.T) { + if !namedotcomLiveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 1) + + provider, err := NewDNSProviderCredentials(namedotcomUsername, namedotcomAPIToken, namedotcomServer) + assert.NoError(t, err) + + err = provider.CleanUp(namedotcomDomain, "", "123d==") + assert.NoError(t, err) +}