From 5f9a0416801fa9ad2b81dcdcbeb6c51d6999bf04 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Tue, 18 Oct 2016 01:20:15 -0700 Subject: [PATCH] Add NS1 DNS provider. (#295) * Add NS1 DNS provider. Integrates Lego with NS1 using their rest API. It uses NS1's official Go package as client: https://github.com/ns1/ns1-go Signed-off-by: David Calavera * Add NS1 to the cli handlers. Signed-off-by: David Calavera --- cli_handlers.go | 3 ++ providers/dns/ns1/ns1.go | 97 +++++++++++++++++++++++++++++++++++ providers/dns/ns1/ns1_test.go | 67 ++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 providers/dns/ns1/ns1.go create mode 100644 providers/dns/ns1/ns1_test.go diff --git a/cli_handlers.go b/cli_handlers.go index 29a1166d..101deb0a 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -24,6 +24,7 @@ import ( "github.com/xenolf/lego/providers/dns/googlecloud" "github.com/xenolf/lego/providers/dns/linode" "github.com/xenolf/lego/providers/dns/namecheap" + "github.com/xenolf/lego/providers/dns/ns1" "github.com/xenolf/lego/providers/dns/ovh" "github.com/xenolf/lego/providers/dns/pdns" "github.com/xenolf/lego/providers/dns/rfc2136" @@ -147,6 +148,8 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { provider, err = ovh.NewDNSProvider() case "pdns": provider, err = pdns.NewDNSProvider() + case "ns1": + provider, err = ns1.NewDNSProvider() } if err != nil { diff --git a/providers/dns/ns1/ns1.go b/providers/dns/ns1/ns1.go new file mode 100644 index 00000000..105d73f8 --- /dev/null +++ b/providers/dns/ns1/ns1.go @@ -0,0 +1,97 @@ +// Package ns1 implements a DNS provider for solving the DNS-01 challenge +// using NS1 DNS. +package ns1 + +import ( + "fmt" + "net/http" + "os" + "time" + + "github.com/xenolf/lego/acme" + "gopkg.in/ns1/ns1-go.v2/rest" + "gopkg.in/ns1/ns1-go.v2/rest/model/dns" +) + +// DNSProvider is an implementation of the acme.ChallengeProvider interface. +type DNSProvider struct { + client *rest.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for NS1. +// Credentials must be passed in the environment variables: NS1_API_KEY. +func NewDNSProvider() (*DNSProvider, error) { + key := os.Getenv("NS1_API_KEY") + if key == "" { + return nil, fmt.Errorf("NS1 credentials missing") + } + return NewDNSProviderCredentials(key) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for NS1. +func NewDNSProviderCredentials(key string) (*DNSProvider, error) { + if key == "" { + return nil, fmt.Errorf("NS1 credentials missing") + } + + httpClient := &http.Client{Timeout: time.Second * 10} + client := rest.NewClient(httpClient, rest.SetAPIKey(key)) + + return &DNSProvider{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) + + zone, err := c.getHostedZone(domain) + if err != nil { + return err + } + + record := c.newTxtRecord(zone, fqdn, value, ttl) + _, err = c.client.Records.Create(record) + if err != nil && err != rest.ErrRecordExists { + return 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) + + zone, err := c.getHostedZone(domain) + if err != nil { + return err + } + + name := acme.UnFqdn(fqdn) + _, err = c.client.Records.Delete(zone.Zone, name, "TXT") + return err +} + +func (c *DNSProvider) getHostedZone(domain string) (*dns.Zone, error) { + zone, _, err := c.client.Zones.Get(domain) + if err != nil { + return nil, err + } + + return zone, nil +} + +func (c *DNSProvider) newTxtRecord(zone *dns.Zone, fqdn, value string, ttl int) *dns.Record { + name := acme.UnFqdn(fqdn) + + return &dns.Record{ + Type: "TXT", + Zone: zone.Zone, + Domain: name, + TTL: ttl, + Answers: []*dns.Answer{ + {Rdata: []string{value}}, + }, + } +} diff --git a/providers/dns/ns1/ns1_test.go b/providers/dns/ns1/ns1_test.go new file mode 100644 index 00000000..eb9150dd --- /dev/null +++ b/providers/dns/ns1/ns1_test.go @@ -0,0 +1,67 @@ +package ns1 + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + liveTest bool + apiKey string + domain string +) + +func init() { + apiKey = os.Getenv("NS1_API_KEY") + domain = os.Getenv("NS1_DOMAIN") + if len(apiKey) > 0 && len(domain) > 0 { + liveTest = true + } +} + +func restoreNS1Env() { + os.Setenv("NS1_API_KEY", apiKey) +} + +func TestNewDNSProviderValid(t *testing.T) { + os.Setenv("NS1_API_KEY", "") + _, err := NewDNSProviderCredentials("123") + assert.NoError(t, err) + restoreNS1Env() +} + +func TestNewDNSProviderMissingCredErr(t *testing.T) { + os.Setenv("NS1_API_KEY", "") + _, err := NewDNSProvider() + assert.EqualError(t, err, "NS1 credentials missing") + restoreNS1Env() +} + +func TestLivePresent(t *testing.T) { + if !liveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProviderCredentials(apiKey) + assert.NoError(t, err) + + err = provider.Present(domain, "", "123d==") + assert.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !liveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 1) + + provider, err := NewDNSProviderCredentials(apiKey) + assert.NoError(t, err) + + err = provider.CleanUp(domain, "", "123d==") + assert.NoError(t, err) +}