From 4e330710a7dd17d7112fdd0eab7bad973e3955cd Mon Sep 17 00:00:00 2001 From: Aaryaman Vasishta Date: Thu, 15 Feb 2018 05:28:02 +0900 Subject: [PATCH 01/12] providers/azure: Refactor to work with Azure SDK version 14.0.0 (#490) --- providers/dns/azure/azure.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/providers/dns/azure/azure.go b/providers/dns/azure/azure.go index 6a30b318..cc15ca7e 100644 --- a/providers/dns/azure/azure.go +++ b/providers/dns/azure/azure.go @@ -4,11 +4,12 @@ package azure import ( + "context" "fmt" "os" "time" - "github.com/Azure/azure-sdk-for-go/arm/dns" + "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2017-09-01/dns" "strings" @@ -26,6 +27,8 @@ type DNSProvider struct { subscriptionId string tenantId string resourceGroup string + + context context.Context } // NewDNSProvider returns a DNSProvider instance configured for azure. @@ -53,6 +56,8 @@ func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, subscriptionId: subscriptionId, tenantId: tenantId, resourceGroup: resourceGroup, + // TODO: A timeout can be added here for cancellation purposes. + context: context.Background(), }, nil } @@ -82,7 +87,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error { TxtRecords: &[]dns.TxtRecord{dns.TxtRecord{Value: &[]string{value}}}, }, } - _, err = rsc.CreateOrUpdate(c.resourceGroup, zone, relative, dns.TXT, rec, "", "") + _, err = rsc.CreateOrUpdate(c.context, c.resourceGroup, zone, relative, dns.TXT, rec, "", "") if err != nil { return err @@ -109,7 +114,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { rsc := dns.NewRecordSetsClient(c.subscriptionId) spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint) rsc.Authorizer = autorest.NewBearerAuthorizer(spt) - _, err = rsc.Delete(c.resourceGroup, zone, relative, dns.TXT, "") + _, err = rsc.Delete(c.context, c.resourceGroup, zone, relative, dns.TXT, "") if err != nil { return err } @@ -130,7 +135,7 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) { dc := dns.NewZonesClient(c.subscriptionId) dc.Authorizer = autorest.NewBearerAuthorizer(spt) - zone, err := dc.Get(c.resourceGroup, acme.UnFqdn(authZone)) + zone, err := dc.Get(c.context, c.resourceGroup, acme.UnFqdn(authZone)) if err != nil { return "", err From bacb545c7a26a247cf36a0c9ec46b2697a65465a Mon Sep 17 00:00:00 2001 From: Derek Chen Date: Sun, 18 Feb 2018 10:27:58 -0500 Subject: [PATCH 02/12] Add DNS provider: Lightsail (#460) * add lightsail dns provider * fix lint errors * update exoscale.go * add the docs for lightsail provider --- cli.go | 1 + providers/dns/dns_providers.go | 5 +- providers/dns/lightsail/lightsail.go | 107 ++++++++++++++++++ .../lightsail/lightsail_integration_test.go | 68 +++++++++++ providers/dns/lightsail/lightsail_test.go | 76 +++++++++++++ providers/dns/lightsail/testutil_test.go | 38 +++++++ 6 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 providers/dns/lightsail/lightsail.go create mode 100644 providers/dns/lightsail/lightsail_integration_test.go create mode 100644 providers/dns/lightsail/lightsail_test.go create mode 100644 providers/dns/lightsail/testutil_test.go diff --git a/cli.go b/cli.go index 5f0dc57d..2f4b8407 100644 --- a/cli.go +++ b/cli.go @@ -213,6 +213,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tgandiv5:\tGANDIV5_API_KEY") fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT, GCE_SERVICE_ACCOUNT_FILE") fmt.Fprintln(w, "\tlinode:\tLINODE_API_KEY") + 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, "\trackspace:\tRACKSPACE_USER, RACKSPACE_API_KEY") diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 06235309..ada957cb 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -17,8 +17,9 @@ import ( "github.com/xenolf/lego/providers/dns/exoscale" "github.com/xenolf/lego/providers/dns/gandi" "github.com/xenolf/lego/providers/dns/gandiv5" - "github.com/xenolf/lego/providers/dns/googlecloud" "github.com/xenolf/lego/providers/dns/godaddy" + "github.com/xenolf/lego/providers/dns/googlecloud" + "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/ns1" @@ -63,6 +64,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) provider, err = googlecloud.NewDNSProvider() case "godaddy": provider, err = godaddy.NewDNSProvider() + case "lightsail": + provider, err = lightsail.NewDNSProvider() case "linode": provider, err = linode.NewDNSProvider() case "manual": diff --git a/providers/dns/lightsail/lightsail.go b/providers/dns/lightsail/lightsail.go new file mode 100644 index 00000000..a4d2efaf --- /dev/null +++ b/providers/dns/lightsail/lightsail.go @@ -0,0 +1,107 @@ +// Package lightsail implements a DNS provider for solving the DNS-01 challenge +// using AWS Lightsail DNS. +package lightsail + +import ( + "math/rand" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/client" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/lightsail" + "github.com/xenolf/lego/acme" +) + +const ( + maxRetries = 5 +) + +// DNSProvider implements the acme.ChallengeProvider interface +type DNSProvider struct { + client *lightsail.Lightsail +} + +// customRetryer implements the client.Retryer interface by composing the +// DefaultRetryer. It controls the logic for retrying recoverable request +// errors (e.g. when rate limits are exceeded). +type customRetryer struct { + client.DefaultRetryer +} + +// RetryRules overwrites the DefaultRetryer's method. +// It uses a basic exponential backoff algorithm that returns an initial +// delay of ~400ms with an upper limit of ~30 seconds which should prevent +// causing a high number of consecutive throttling errors. +// For reference: Route 53 enforces an account-wide(!) 5req/s query limit. +func (d customRetryer) RetryRules(r *request.Request) time.Duration { + retryCount := r.RetryCount + if retryCount > 7 { + retryCount = 7 + } + + delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200) + return time.Duration(delay) * time.Millisecond +} + +// NewDNSProvider returns a DNSProvider instance configured for the AWS +// Lightsail service. +// +// AWS Credentials are automatically detected in the following locations +// and prioritized in the following order: +// 1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, +// [AWS_SESSION_TOKEN], [DNS_ZONE] +// 2. Shared credentials file (defaults to ~/.aws/credentials) +// 3. Amazon EC2 IAM role +// +// public hosted zone via the FQDN. +// +// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk +func NewDNSProvider() (*DNSProvider, error) { + r := customRetryer{} + r.NumMaxRetries = maxRetries + config := request.WithRetryer(aws.NewConfig(), r) + client := lightsail.New(session.New(config)) + + return &DNSProvider{ + client: client, + }, nil +} + +// Present creates a TXT record using the specified parameters +func (r *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + value = `"` + value + `"` + err := r.newTxtRecord(domain, fqdn, value) + return err +} + +// CleanUp removes the TXT record matching the specified parameters +func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + value = `"` + value + `"` + params := &lightsail.DeleteDomainEntryInput{ + DomainName: aws.String(domain), + DomainEntry: &lightsail.DomainEntry{ + Name: aws.String(fqdn), + Type: aws.String("TXT"), + Target: aws.String(value), + }, + } + _, err := r.client.DeleteDomainEntry(params) + return err +} + +func (r *DNSProvider) newTxtRecord(domain string, fqdn string, value string) error { + params := &lightsail.CreateDomainEntryInput{ + DomainName: aws.String(domain), + DomainEntry: &lightsail.DomainEntry{ + Name: aws.String(fqdn), + Target: aws.String(value), + Type: aws.String("TXT"), + }, + } + _, err := r.client.CreateDomainEntry(params) + return err +} diff --git a/providers/dns/lightsail/lightsail_integration_test.go b/providers/dns/lightsail/lightsail_integration_test.go new file mode 100644 index 00000000..ee6216ea --- /dev/null +++ b/providers/dns/lightsail/lightsail_integration_test.go @@ -0,0 +1,68 @@ +package lightsail + +import ( + "fmt" + "os" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/lightsail" +) + +func TestLightsailTTL(t *testing.T) { + + m, err := testGetAndPreCheck() + if err != nil { + t.Skip(err.Error()) + } + + provider, err := NewDNSProvider() + if err != nil { + t.Fatalf("Fatal: %s", err.Error()) + } + + err = provider.Present(m["lightsailDomain"], "foo", "bar") + if err != nil { + t.Fatalf("Fatal: %s", err.Error()) + } + // we need a separate Lightshail client here as the one in the DNS provider is + // unexported. + fqdn := "_acme-challenge." + m["lightsailDomain"] + svc := lightsail.New(session.New()) + if err != nil { + provider.CleanUp(m["lightsailDomain"], "foo", "bar") + t.Fatalf("Fatal: %s", err.Error()) + } + params := &lightsail.GetDomainInput{ + DomainName: aws.String(m["lightsailDomain"]), + } + resp, err := svc.GetDomain(params) + if err != nil { + provider.CleanUp(m["lightsailDomain"], "foo", "bar") + t.Fatalf("Fatal: %s", err.Error()) + } + entries := resp.Domain.DomainEntries + for _, entry := range entries { + if *entry.Type == "TXT" && *entry.Name == fqdn { + provider.CleanUp(m["lightsailDomain"], "foo", "bar") + return + } + } + provider.CleanUp(m["lightsailDomain"], "foo", "bar") + t.Fatalf("Could not find a TXT record for _acme-challenge.%s", m["lightsailDomain"]) +} + +func testGetAndPreCheck() (map[string]string, error) { + m := map[string]string{ + "lightsailKey": os.Getenv("AWS_ACCESS_KEY_ID"), + "lightsailSecret": os.Getenv("AWS_SECRET_ACCESS_KEY"), + "lightsailDomain": os.Getenv("DNS_ZONE"), + } + for _, v := range m { + if v == "" { + return nil, fmt.Errorf("AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and R53_DOMAIN are needed to run this test") + } + } + return m, nil +} diff --git a/providers/dns/lightsail/lightsail_test.go b/providers/dns/lightsail/lightsail_test.go new file mode 100644 index 00000000..d443da54 --- /dev/null +++ b/providers/dns/lightsail/lightsail_test.go @@ -0,0 +1,76 @@ +package lightsail + +import ( + "net/http/httptest" + "os" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/lightsail" + "github.com/stretchr/testify/assert" +) + +var ( + lightsailSecret string + lightsailKey string + lightsailZone string +) + +func init() { + lightsailKey = os.Getenv("AWS_ACCESS_KEY_ID") + lightsailSecret = os.Getenv("AWS_SECRET_ACCESS_KEY") +} + +func restoreLightsailEnv() { + os.Setenv("AWS_ACCESS_KEY_ID", lightsailKey) + os.Setenv("AWS_SECRET_ACCESS_KEY", lightsailSecret) + os.Setenv("AWS_REGION", "us-east-1") + os.Setenv("AWS_HOSTED_ZONE_ID", lightsailZone) +} + +func makeLightsailProvider(ts *httptest.Server) *DNSProvider { + config := &aws.Config{ + Credentials: credentials.NewStaticCredentials("abc", "123", " "), + Endpoint: aws.String(ts.URL), + Region: aws.String("mock-region"), + MaxRetries: aws.Int(1), + } + + client := lightsail.New(session.New(config)) + return &DNSProvider{client: client} +} + +func TestCredentialsFromEnv(t *testing.T) { + os.Setenv("AWS_ACCESS_KEY_ID", "123") + os.Setenv("AWS_SECRET_ACCESS_KEY", "123") + os.Setenv("AWS_REGION", "us-east-1") + + config := &aws.Config{ + CredentialsChainVerboseErrors: aws.Bool(true), + } + + sess := session.New(config) + _, err := sess.Config.Credentials.Get() + assert.NoError(t, err, "Expected credentials to be set from environment") + + restoreLightsailEnv() +} + +func TestLightsailPresent(t *testing.T) { + mockResponses := MockResponseMap{ + "/": MockResponse{StatusCode: 200, Body: ""}, + } + + ts := newMockServer(t, mockResponses) + defer ts.Close() + + provider := makeLightsailProvider(ts) + + domain := "example.com" + keyAuth := "123456d==" + + err := provider.Present(domain, "", keyAuth) + assert.NoError(t, err, "Expected Present to return no error") +} diff --git a/providers/dns/lightsail/testutil_test.go b/providers/dns/lightsail/testutil_test.go new file mode 100644 index 00000000..11141216 --- /dev/null +++ b/providers/dns/lightsail/testutil_test.go @@ -0,0 +1,38 @@ +package lightsail + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// MockResponse represents a predefined response used by a mock server +type MockResponse struct { + StatusCode int + Body string +} + +// MockResponseMap maps request paths to responses +type MockResponseMap map[string]MockResponse + +func newMockServer(t *testing.T, responses MockResponseMap) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + resp, ok := responses[path] + if !ok { + msg := fmt.Sprintf("Requested path not found in response map: %s", path) + require.FailNow(t, msg) + } + + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(resp.StatusCode) + w.Write([]byte(resp.Body)) + })) + + time.Sleep(100 * time.Millisecond) + return ts +} From 91b13b10b9b5f0fa32454a4249de26936c72a201 Mon Sep 17 00:00:00 2001 From: Pat Moroney Date: Wed, 14 Mar 2018 11:43:09 -0600 Subject: [PATCH 03/12] 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) +} From 2e0e9cd68f0dd462a61a353129ac720b16b7883b Mon Sep 17 00:00:00 2001 From: Remi Broemeling Date: Mon, 19 Mar 2018 10:41:57 -0600 Subject: [PATCH 04/12] Slightly improve Dyn provider error reporting. (#473) If Dyn responds with a 3xx or 4xx status code, information describing exactly what went wrong is generally included in the body of the response (as part of the typical Dyn JSON response). On the other hand, if Dyn responds with a 5xx status code, we very likely have extremely limited information. This commit modifies the reporting to display the explanatory messages included in the body of the Dyn response for 3xx and 4xx status codes. The intent is to make it much easier to determine what might be going wrong (when something is going wrong). --- providers/dns/dyn/dyn.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/providers/dns/dyn/dyn.go b/providers/dns/dyn/dyn.go index 384bc850..277dffb9 100644 --- a/providers/dns/dyn/dyn.go +++ b/providers/dns/dyn/dyn.go @@ -87,11 +87,8 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) } defer resp.Body.Close() - if resp.StatusCode >= 400 { + if resp.StatusCode >= 500 { return nil, fmt.Errorf("Dyn API request failed with HTTP status code %d", resp.StatusCode) - } else if resp.StatusCode == 307 { - // TODO add support for HTTP 307 response and long running jobs - return nil, fmt.Errorf("Dyn API request returned HTTP 307. This is currently unsupported") } var dynRes dynResponse @@ -100,6 +97,13 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) return nil, err } + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("Dyn API request failed with HTTP status code %d: %s", resp.StatusCode, dynRes.Messages) + } else if resp.StatusCode == 307 { + // TODO add support for HTTP 307 response and long running jobs + return nil, fmt.Errorf("Dyn API request returned HTTP 307. This is currently unsupported") + } + if dynRes.Status == "failure" { // TODO add better error handling return nil, fmt.Errorf("Dyn API request failed: %s", dynRes.Messages) From 2b18d40babe3f1b6d9609172a02c90f64c172a26 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Tue, 27 Mar 2018 16:10:38 +0200 Subject: [PATCH 05/12] Add DNS challenge provider 'exec' (#508) As discussed in #505, this commits adds a very simple DNS provider which calls out to an external program which must then add or remove the DNS record. --- cli.go | 1 + providers/dns/dns_providers.go | 3 ++ providers/dns/exec/exec.go | 72 ++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 providers/dns/exec/exec.go diff --git a/cli.go b/cli.go index 95801173..aed50127 100644 --- a/cli.go +++ b/cli.go @@ -226,6 +226,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tpdns:\tPDNS_API_KEY, PDNS_API_URL") fmt.Fprintln(w, "\tdnspod:\tDNSPOD_API_KEY") fmt.Fprintln(w, "\totc:\tOTC_USER_NAME, OTC_PASSWORD, OTC_PROJECT_NAME, OTC_DOMAIN_NAME, OTC_IDENTITY_ENDPOINT") + fmt.Fprintln(w, "\texec:\tEXEC_PATH") w.Flush() fmt.Println(` diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index b53c249f..437aa499 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -14,6 +14,7 @@ import ( "github.com/xenolf/lego/providers/dns/dnsmadeeasy" "github.com/xenolf/lego/providers/dns/dnspod" "github.com/xenolf/lego/providers/dns/dyn" + "github.com/xenolf/lego/providers/dns/exec" "github.com/xenolf/lego/providers/dns/exoscale" "github.com/xenolf/lego/providers/dns/gandi" "github.com/xenolf/lego/providers/dns/gandiv5" @@ -91,6 +92,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) provider, err = ns1.NewDNSProvider() case "otc": provider, err = otc.NewDNSProvider() + case "exec": + provider, err = exec.NewDNSProvider() default: err = fmt.Errorf("Unrecognised DNS provider: %s", name) } diff --git a/providers/dns/exec/exec.go b/providers/dns/exec/exec.go new file mode 100644 index 00000000..ee140ae7 --- /dev/null +++ b/providers/dns/exec/exec.go @@ -0,0 +1,72 @@ +// Package exec implements a manual DNS provider which runs a program for +// adding/removing the DNS record. +// +// The file name of the external program is specified in the environment +// variable EXEC_PATH. When it is run by lego, three command-line parameters +// are passed to it: The action ("present" or "cleanup"), the fully-qualified domain +// name, the value for the record and the TTL. +// +// For example, requesting a certificate for the domain 'foo.example.com' can +// be achieved by calling lego as follows: +// +// EXEC_PATH=./update-dns.sh \ +// lego --dns exec \ +// --domains foo.example.com \ +// --email invalid@example.com run +// +// It will then call the program './update-dns.sh' with like this: +// +// ./update-dns.sh "present" "_acme-challenge.foo.example.com." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI" "120" +// +// The program then needs to make sure the record is inserted. When it returns +// an error via a non-zero exit code, lego aborts. +// +// When the record is to be removed again, the program is called with the first +// command-line parameter set to "cleanup" instead of "present". +package exec + +import ( + "errors" + "os" + "os/exec" + "strconv" + + "github.com/xenolf/lego/acme" +) + +// DNSProvider adds and removes the record for the DNS challenge by calling a +// program with command-line parameters. +type DNSProvider struct { + program string +} + +// NewDNSProvider returns a new DNS provider which runs the program in the +// environment variable EXEC_PATH for adding and removing the DNS record. +func NewDNSProvider() (*DNSProvider, error) { + s := os.Getenv("EXEC_PATH") + if s == "" { + return nil, errors.New("environment variable EXEC_PATH not set") + } + + return &DNSProvider{program: s}, nil +} + +// Present creates a TXT record to fulfil the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) + cmd := exec.Command(d.program, "present", fqdn, value, strconv.Itoa(ttl)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +// CleanUp removes the TXT record matching the specified parameters +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) + cmd := exec.Command(d.program, "cleanup", fqdn, value, strconv.Itoa(ttl)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} From 1028c3b19077ea5797d7c6b84eafd3b3a500f8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Bjug=C3=A5rd?= Date: Sat, 31 Mar 2018 16:33:48 +0200 Subject: [PATCH 06/12] Add DNS-01 solver using the GleSYS API (#502) * Add GleSYS DNS-01 solver * API url is not overridden during tests * Use logging package * Correct documentation for NewDNSProvider --- cli.go | 1 + providers/dns/dns_providers.go | 3 + providers/dns/glesys/glesys.go | 211 ++++++++++++++++++++++++++++ providers/dns/glesys/glesys_test.go | 60 ++++++++ 4 files changed, 275 insertions(+) create mode 100644 providers/dns/glesys/glesys.go create mode 100644 providers/dns/glesys/glesys_test.go diff --git a/cli.go b/cli.go index aed50127..faacf748 100644 --- a/cli.go +++ b/cli.go @@ -212,6 +212,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tgandi:\tGANDI_API_KEY") fmt.Fprintln(w, "\tgandiv5:\tGANDIV5_API_KEY") fmt.Fprintln(w, "\tgcloud:\tGCE_PROJECT, GCE_SERVICE_ACCOUNT_FILE") + fmt.Fprintln(w, "\tglesys:\tGLESYS_API_USER, GLESYS_API_KEY") fmt.Fprintln(w, "\tlinode:\tLINODE_API_KEY") fmt.Fprintln(w, "\tlightsail:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, DNS_ZONE") fmt.Fprintln(w, "\tmanual:\tnone") diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 437aa499..d507be08 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -18,6 +18,7 @@ import ( "github.com/xenolf/lego/providers/dns/exoscale" "github.com/xenolf/lego/providers/dns/gandi" "github.com/xenolf/lego/providers/dns/gandiv5" + "github.com/xenolf/lego/providers/dns/glesys" "github.com/xenolf/lego/providers/dns/godaddy" "github.com/xenolf/lego/providers/dns/googlecloud" "github.com/xenolf/lego/providers/dns/lightsail" @@ -62,6 +63,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) provider, err = gandi.NewDNSProvider() case "gandiv5": provider, err = gandiv5.NewDNSProvider() + case "glesys": + provider, err = glesys.NewDNSProvider() case "gcloud": provider, err = googlecloud.NewDNSProvider() case "godaddy": diff --git a/providers/dns/glesys/glesys.go b/providers/dns/glesys/glesys.go new file mode 100644 index 00000000..36c6c00d --- /dev/null +++ b/providers/dns/glesys/glesys.go @@ -0,0 +1,211 @@ +// Package glesys implements a DNS provider for solving the DNS-01 +// challenge using GleSYS api. +package glesys + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strings" + "sync" + "time" + + "github.com/xenolf/lego/acme" +) + +// GleSYS API reference: https://github.com/GleSYS/API/wiki/API-Documentation + +// domainAPI is the GleSYS API endpoint used by Present and CleanUp. +const domainAPI = "https://api.glesys.com/domain" + +var ( + // Logger is used to log API communication results; + // if nil, the default log.Logger is used. + Logger *log.Logger +) + +// logf writes a log entry. It uses Logger if not +// nil, otherwise it uses the default log.Logger. +func logf(format string, args ...interface{}) { + if Logger != nil { + Logger.Printf(format, args...) + } else { + log.Printf(format, args...) + } +} + +// DNSProvider is an implementation of the +// acme.ChallengeProviderTimeout interface that uses GleSYS +// API to manage TXT records for a domain. +type DNSProvider struct { + apiUser string + apiKey string + activeRecords map[string]int + inProgressMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for GleSYS. +// Credentials must be passed in the environment variables: GLESYS_API_USER +// and GLESYS_API_KEY. +func NewDNSProvider() (*DNSProvider, error) { + apiUser := os.Getenv("GLESYS_API_USER") + apiKey := os.Getenv("GLESYS_API_KEY") + return NewDNSProviderCredentials(apiUser, apiKey) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for GleSYS. +func NewDNSProviderCredentials(apiUser string, apiKey string) (*DNSProvider, error) { + if apiUser == "" || apiKey == "" { + return nil, fmt.Errorf("GleSYS DNS: Incomplete credentials provided") + } + return &DNSProvider{ + apiUser: apiUser, + apiKey: apiKey, + activeRecords: make(map[string]int), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) + if ttl < 60 { + ttl = 60 // 60 is GleSYS minimum value for ttl + } + // find authZone + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return fmt.Errorf("GleSYS DNS: findZoneByFqdn failure: %v", err) + } + // determine name of TXT record + if !strings.HasSuffix( + strings.ToLower(fqdn), strings.ToLower("."+authZone)) { + return fmt.Errorf( + "GleSYS DNS: unexpected authZone %s for fqdn %s", authZone, fqdn) + } + name := fqdn[:len(fqdn)-len("."+authZone)] + // acquire lock and check there is not a challenge already in + // progress for this value of authZone + d.inProgressMu.Lock() + defer d.inProgressMu.Unlock() + // add TXT record into authZone + recordId, err := d.addTXTRecord(domain, acme.UnFqdn(authZone), name, value, ttl) + if err != nil { + return err + } + // save data necessary for CleanUp + d.activeRecords[fqdn] = recordId + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + // acquire lock and retrieve authZone + d.inProgressMu.Lock() + defer d.inProgressMu.Unlock() + if _, ok := d.activeRecords[fqdn]; !ok { + // if there is no cleanup information then just return + return nil + } + recordId := d.activeRecords[fqdn] + delete(d.activeRecords, fqdn) + // delete TXT record from authZone + err := d.deleteTXTRecord(domain, recordId) + if err != nil { + return err + } + return nil +} + +// Timeout returns the values (20*time.Minute, 20*time.Second) which +// are used by the acme package as timeout and check interval values +// when checking for DNS record propagation with GleSYS. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return 20 * time.Minute, 20 * time.Second +} + +// types for JSON method calls, parameters, and responses + +type addRecordRequest struct { + Domainname string `json:"domainname"` + Host string `json:"host"` + Type string `json:"type"` + Data string `json:"data"` + Ttl int `json:"ttl,omitempty"` +} + +type deleteRecordRequest struct { + Recordid int `json:"recordid"` +} + +type responseStruct struct { + Response struct { + Status struct { + Code int `json:"code"` + } `json:"status"` + Record deleteRecordRequest `json:"record"` + } `json:"response"` +} + +// POSTing/Marshalling/Unmarshalling + +func (d *DNSProvider) sendRequest(method string, resource string, payload interface{}) (*responseStruct, error) { + url := fmt.Sprintf("%s/%s", domainAPI, resource) + + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + req, err := http.NewRequest(method, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.SetBasicAuth(d.apiUser, d.apiKey) + + client := &http.Client{Timeout: time.Duration(10 * time.Second)} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("GleSYS DNS: request failed with HTTP status code %d", resp.StatusCode) + } + var response responseStruct + err = json.NewDecoder(resp.Body).Decode(&response) + + return &response, err +} + +// functions to perform API actions + +func (d *DNSProvider) addTXTRecord(fqdn string, domain string, name string, value string, ttl int) (int, error) { + response, err := d.sendRequest("POST", "addrecord", addRecordRequest{ + Domainname: domain, + Host: name, + Type: "TXT", + Data: value, + Ttl: ttl, + }) + if response != nil && response.Response.Status.Code == 200 { + logf("[INFO][%s] GleSYS DNS: Successfully created recordid %d", fqdn, response.Response.Record.Recordid) + return response.Response.Record.Recordid, nil + } + return 0, err +} + +func (d *DNSProvider) deleteTXTRecord(fqdn string, recordid int) error { + response, err := d.sendRequest("POST", "deleterecord", deleteRecordRequest{ + Recordid: recordid, + }) + if response != nil && response.Response.Status.Code == 200 { + logf("[INFO][%s] GleSYS DNS: Successfully deleted recordid %d", fqdn, recordid) + } + return err +} diff --git a/providers/dns/glesys/glesys_test.go b/providers/dns/glesys/glesys_test.go new file mode 100644 index 00000000..c10ba3a7 --- /dev/null +++ b/providers/dns/glesys/glesys_test.go @@ -0,0 +1,60 @@ +package glesys + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + glesysAPIUser string + glesysAPIKey string + glesysDomain string + glesysLiveTest bool +) + +func init() { + glesysAPIUser = os.Getenv("GLESYS_API_USER") + glesysAPIKey = os.Getenv("GLESYS_API_KEY") + glesysDomain = os.Getenv("GLESYS_DOMAIN") + + if len(glesysAPIUser) > 0 && len(glesysAPIKey) > 0 && len(glesysDomain) > 0 { + glesysLiveTest = true + } +} + +func TestNewDNSProvider(t *testing.T) { + provider, err := NewDNSProvider() + + if !glesysLiveTest { + assert.Error(t, err) + } else { + assert.NotNil(t, provider) + assert.NoError(t, err) + } +} + +func TestDNSProvider_Present(t *testing.T) { + if !glesysLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.Present(glesysDomain, "", "123d==") + assert.NoError(t, err) +} + +func TestDNSProvider_CleanUp(t *testing.T) { + if !glesysLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.CleanUp(glesysDomain, "", "123d==") + assert.NoError(t, err) +} From d7fdc8f54aecdaba627aedc1d740ee1cc2d3a26c Mon Sep 17 00:00:00 2001 From: Nick Maliwacki Date: Mon, 2 Apr 2018 07:02:54 -0700 Subject: [PATCH 07/12] Add dns provider duckdns.org (#513) * Add dns provider duckdns see http://www.duckdns.org/spec.jsp for more info * Add DNS challenge provider 'exec' (#508) As discussed in #505, this commits adds a very simple DNS provider which calls out to an external program which must then add or remove the DNS record. * Update duckdns to support caddy, and cleanup some comments --- cli.go | 1 + providers/dns/dns_providers.go | 3 + providers/dns/duckdns/duckdns.go | 82 +++++++++++++++++++++++++++ providers/dns/duckdns/duckdns_test.go | 65 +++++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 providers/dns/duckdns/duckdns.go create mode 100644 providers/dns/duckdns/duckdns_test.go diff --git a/cli.go b/cli.go index faacf748..bd61498e 100644 --- a/cli.go +++ b/cli.go @@ -208,6 +208,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN") fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_OAUTH_TOKEN") fmt.Fprintln(w, "\tdnsmadeeasy:\tDNSMADEEASY_API_KEY, DNSMADEEASY_API_SECRET") + fmt.Fprintln(w, "\tduckdns:\tDUCKDNS_TOKEN") fmt.Fprintln(w, "\texoscale:\tEXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT") fmt.Fprintln(w, "\tgandi:\tGANDI_API_KEY") fmt.Fprintln(w, "\tgandiv5:\tGANDIV5_API_KEY") diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index d507be08..931e3a5b 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -13,6 +13,7 @@ import ( "github.com/xenolf/lego/providers/dns/dnsimple" "github.com/xenolf/lego/providers/dns/dnsmadeeasy" "github.com/xenolf/lego/providers/dns/dnspod" + "github.com/xenolf/lego/providers/dns/duckdns" "github.com/xenolf/lego/providers/dns/dyn" "github.com/xenolf/lego/providers/dns/exec" "github.com/xenolf/lego/providers/dns/exoscale" @@ -55,6 +56,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) provider, err = dnsmadeeasy.NewDNSProvider() case "dnspod": provider, err = dnspod.NewDNSProvider() + case "duckdns": + provider, err = duckdns.NewDNSProvider() case "dyn": provider, err = dyn.NewDNSProvider() case "exoscale": diff --git a/providers/dns/duckdns/duckdns.go b/providers/dns/duckdns/duckdns.go new file mode 100644 index 00000000..6e2102a7 --- /dev/null +++ b/providers/dns/duckdns/duckdns.go @@ -0,0 +1,82 @@ +// Adds lego support for http://duckdns.org . +// +// See http://www.duckdns.org/spec.jsp for more info on updating TXT records. +package duckdns + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + + "github.com/xenolf/lego/acme" +) + +// DNSProvider adds and removes the record for the DNS challenge +type DNSProvider struct { + // The duckdns api token + token string +} + +// NewDNSProvider returns a new DNS provider using +// environment variable DUCKDNS_TOKEN for adding and removing the DNS record. +func NewDNSProvider() (*DNSProvider, error) { + duckdnsToken := os.Getenv("DUCKDNS_TOKEN") + + return NewDNSProviderCredentials(duckdnsToken) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for http://duckdns.org . +func NewDNSProviderCredentials(duckdnsToken string) (*DNSProvider, error) { + if duckdnsToken == "" { + return nil, errors.New("environment variable DUCKDNS_TOKEN not set") + } + + return &DNSProvider{token: duckdnsToken}, nil +} + +// makeDuckdnsURL creates a url to clear the set or unset the TXT record. +// txt == "" will clear the TXT record. +func makeDuckdnsURL(domain, token, txt string) string { + requestBase := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s", domain, token) + if txt == "" { + return requestBase + "&clear=true" + } + return requestBase + "&txt=" + txt +} + +func issueDuckdnsRequest(url string) error { + response, err := acme.HTTPClient.Get(url) + if err != nil { + return err + } + defer response.Body.Close() + + bodyBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + body := string(bodyBytes) + if body != "OK" { + return fmt.Errorf("Request to change TXT record for duckdns returned the following result (%s) this does not match expectation (OK) used url [%s]", body, url) + } + return nil +} + +// Present creates a TXT record to fulfil the dns-01 challenge. +// In duckdns you only have one TXT record shared with +// the domain and all sub domains. +// +// To update the TXT record we just need to make one simple get request. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + _, txtRecord, _ := acme.DNS01Record(domain, keyAuth) + url := makeDuckdnsURL(domain, d.token, txtRecord) + return issueDuckdnsRequest(url) +} + +// CleanUp clears duckdns TXT record +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + url := makeDuckdnsURL(domain, d.token, "") + return issueDuckdnsRequest(url) +} diff --git a/providers/dns/duckdns/duckdns_test.go b/providers/dns/duckdns/duckdns_test.go new file mode 100644 index 00000000..f1afed4f --- /dev/null +++ b/providers/dns/duckdns/duckdns_test.go @@ -0,0 +1,65 @@ +package duckdns + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + duckdnsLiveTest bool + duckdnsToken string + duckdnsDomain string +) + +func init() { + duckdnsToken = os.Getenv("DUCKDNS_TOKEN") + duckdnsDomain = os.Getenv("DUCKDNS_DOMAIN") + if len(duckdnsDomain) > 0 && len(duckdnsDomain) > 0 { + duckdnsLiveTest = true + } +} + +func restoreDuckdnsEnv() { + os.Setenv("DUCKDNS_TOKEN", duckdnsToken) +} + +func TestNewDNSProviderValidEnv(t *testing.T) { + os.Setenv("DUCKDNS_TOKEN", "123") + _, err := NewDNSProvider() + assert.NoError(t, err) + restoreDuckdnsEnv() +} +func TestNewDNSProviderMissingCredErr(t *testing.T) { + os.Setenv("DUCKDNS_TOKEN", "") + _, err := NewDNSProvider() + assert.EqualError(t, err, "environment variable DUCKDNS_TOKEN not set") + restoreDuckdnsEnv() +} +func TestLiveDuckdnsPresent(t *testing.T) { + if !duckdnsLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.Present(duckdnsDomain, "", "123d==") + assert.NoError(t, err) +} + +func TestLiveDuckdnsCleanUp(t *testing.T) { + if !duckdnsLiveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 10) + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.CleanUp(duckdnsDomain, "", "123d==") + assert.NoError(t, err) +} From 5ebb80fb44c9a8bdb36542ca73e2cf9674c1010b Mon Sep 17 00:00:00 2001 From: Kirby Files Date: Mon, 2 Apr 2018 22:50:15 -0400 Subject: [PATCH 08/12] Add Bluecat DNS provider (#483) --- cli.go | 1 + providers/dns/bluecat/bluecat.go | 418 ++++++++++++++++++++++++++ providers/dns/bluecat/bluecat_test.go | 57 ++++ providers/dns/dns_providers.go | 3 + 4 files changed, 479 insertions(+) create mode 100644 providers/dns/bluecat/bluecat.go create mode 100644 providers/dns/bluecat/bluecat_test.go diff --git a/cli.go b/cli.go index bd61498e..3878e095 100644 --- a/cli.go +++ b/cli.go @@ -203,6 +203,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w) fmt.Fprintln(w, "\tazure:\tAZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP") fmt.Fprintln(w, "\tauroradns:\tAURORA_USER_ID, AURORA_KEY, AURORA_ENDPOINT") + fmt.Fprintln(w, "\tbluecat:\tBLUECAT_SERVER_URL, BLUECAT_USER_NAME, BLUECAT_PASSWORD, BLUECAT_CONFIG_NAME, BLUECAT_DNS_VIEW") fmt.Fprintln(w, "\tcloudxns:\tCLOUDXNS_API_KEY, CLOUDXNS_SECRET_KEY") fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY") fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN") diff --git a/providers/dns/bluecat/bluecat.go b/providers/dns/bluecat/bluecat.go new file mode 100644 index 00000000..92b8a21d --- /dev/null +++ b/providers/dns/bluecat/bluecat.go @@ -0,0 +1,418 @@ +// Package bluecat implements a DNS provider for solving the DNS-01 challenge +// using a self-hosted Bluecat Address Manager. +package bluecat + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/xenolf/lego/acme" + "io/ioutil" +) + +const bluecatUrlTemplate = "%s/Services/REST/v1" +const configType = "Configuration" +const viewType = "View" +const txtType = "TXTRecord" +const zoneType = "Zone" + +type entityResponse struct { + Id uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Properties string `json:"properties"` +} + +// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses +// Bluecat's Address Manager REST API to manage TXT records for a domain. +type DNSProvider struct { + baseUrl string + userName string + password string + configName string + dnsView string + token string + httpClient *http.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Bluecat DNS. +// Credentials must be passed in the environment variables: BLUECAT_SERVER_URL, +// BLUECAT_USER_NAME and BLUECAT_PASSWORD. BLUECAT_SERVER_URL should have the +// scheme, hostname, and port (if required) of the authoritative Bluecat BAM +// server. The REST endpoint will be appended. In addition, the Configuration name +// and external DNS View Name must be passed in BLUECAT_CONFIG_NAME and +// BLUECAT_DNS_VIEW +func NewDNSProvider() (*DNSProvider, error) { + server := os.Getenv("BLUECAT_SERVER_URL") + userName := os.Getenv("BLUECAT_USER_NAME") + password := os.Getenv("BLUECAT_PASSWORD") + configName := os.Getenv("BLUECAT_CONFIG_NAME") + dnsView := os.Getenv("BLUECAT_DNS_VIEW") + httpClient := http.Client{Timeout: time.Duration(30 * time.Second)} + return NewDNSProviderCredentials(server, userName, password, configName, dnsView, httpClient) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for Bluecat DNS. +func NewDNSProviderCredentials(server, userName, password, configName, dnsView string, httpClient http.Client) (*DNSProvider, error) { + if server == "" || userName == "" || password == "" || configName == "" || dnsView == "" { + return nil, fmt.Errorf("Bluecat credentials missing") + } + + return &DNSProvider{ + baseUrl: fmt.Sprintf(bluecatUrlTemplate, server), + userName: userName, + password: password, + configName: configName, + dnsView: dnsView, + httpClient: http.DefaultClient, + }, nil +} + +// Send a REST request, using query parameters specified. The Authorization +// header will be set if we have an active auth token +func (d *DNSProvider) sendRequest(method, resource string, payload interface{}, queryArgs map[string]string) (*http.Response, error) { + url := fmt.Sprintf("%s/%s", d.baseUrl, resource) + + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(method, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + if len(d.token) > 0 { + req.Header.Set("Authorization", d.token) + } + + // Add all query parameters + q := req.URL.Query() + for argName, argVal := range queryArgs { + q.Add(argName, argVal) + } + req.URL.RawQuery = q.Encode() + resp, err := d.httpClient.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode >= 400 { + errBytes, _ := ioutil.ReadAll(resp.Body) + errResp := string(errBytes) + return nil, fmt.Errorf("Bluecat API request failed with HTTP status code %d\n Full message: %s", + resp.StatusCode, errResp) + } + + return resp, nil +} + +// Starts a new Bluecat API Session. Authenticates using customerName, userName, +// password and receives a token to be used in for subsequent requests. +func (d *DNSProvider) login() error { + queryArgs := map[string]string{ + "username": d.userName, + "password": d.password, + } + + resp, err := d.sendRequest("GET", "login", nil, queryArgs) + if err != nil { + return err + } + defer resp.Body.Close() + + authBytes, _ := ioutil.ReadAll(resp.Body) + authResp := string(authBytes) + + if strings.Contains(authResp, "Authentication Error") { + msg := strings.Trim(authResp, "\"") + return fmt.Errorf("Bluecat API request failed: %s", msg) + } + // Upon success, API responds with "Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username" + re := regexp.MustCompile("BAMAuthToken: [^ ]+") + token := re.FindString(authResp) + d.token = token + return nil +} + +// Destroys Bluecat Session +func (d *DNSProvider) logout() error { + if len(d.token) == 0 { + // nothing to do + return nil + } + + resp, err := d.sendRequest("GET", "logout", nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("Bluecat API request failed to delete session with HTTP status code %d", resp.StatusCode) + } else { + authBytes, _ := ioutil.ReadAll(resp.Body) + authResp := string(authBytes) + + if !strings.Contains(authResp, "successfully") { + msg := strings.Trim(authResp, "\"") + return fmt.Errorf("Bluecat API request failed to delete session: %s", msg) + } + } + + d.token = "" + + return nil +} + +// Lookup the entity ID of the configuration named in our properties +func (d *DNSProvider) lookupConfId() (uint, error) { + queryArgs := map[string]string{ + "parentId": strconv.Itoa(0), + "name": d.configName, + "type": configType, + } + + resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + var conf entityResponse + err = json.NewDecoder(resp.Body).Decode(&conf) + if err != nil { + return 0, err + } + return conf.Id, nil +} + +// Find the DNS view with the given name within +func (d *DNSProvider) lookupViewId(viewName string) (uint, error) { + confId, err := d.lookupConfId() + if err != nil { + return 0, err + } + + queryArgs := map[string]string{ + "parentId": strconv.FormatUint(uint64(confId), 10), + "name": d.dnsView, + "type": viewType, + } + + resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + var view entityResponse + err = json.NewDecoder(resp.Body).Decode(&view) + if err != nil { + return 0, err + } + + return view.Id, nil +} + +// Return the entityId of the parent zone by recursing from the root view +// Also return the simple name of the host +func (d *DNSProvider) lookupParentZoneId(viewId uint, fqdn string) (uint, string, error) { + parentViewId := viewId + name := "" + + if fqdn != "" { + zones := strings.Split(strings.Trim(fqdn, "."), ".") + last := len(zones) - 1 + name = zones[0] + + for i := last; i > -1; i-- { + zoneId, err := d.getZone(parentViewId, zones[i]) + if err != nil || zoneId == 0 { + return parentViewId, name, err + } + if (i > 0) { + name = strings.Join(zones[0:i],".") + } + parentViewId = zoneId + } + } + + return parentViewId, name, nil +} + +// Get the DNS zone with the specified name under the parentId +func (d *DNSProvider) getZone(parentId uint, name string) (uint, error) { + + queryArgs := map[string]string{ + "parentId": strconv.FormatUint(uint64(parentId), 10), + "name": name, + "type": zoneType, + } + + resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs) + // Return an empty zone if the named zone doesn't exist + if resp != nil && resp.StatusCode == 404 { + return 0, fmt.Errorf("Bluecat API could not find zone named %s", name) + } + if err != nil { + return 0, err + } + defer resp.Body.Close() + + var zone entityResponse + err = json.NewDecoder(resp.Body).Decode(&zone) + if err != nil { + return 0, err + } + + return zone.Id, nil +} + +// Present creates a TXT record using the specified parameters +// This will *not* create a subzone to contain the TXT record, +// so make sure the FQDN specified is within an extant zone. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) + + err := d.login() + if err != nil { + return err + } + + viewId, err := d.lookupViewId(d.dnsView) + if err != nil { + return err + } + + parentZoneId, name, err := d.lookupParentZoneId(viewId, fqdn) + + queryArgs := map[string]string{ + "parentId": strconv.FormatUint(uint64(parentZoneId), 10), + } + + body := bluecatEntity{ + Name: name, + Type: "TXTRecord", + Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", ttl, fqdn, value), + } + + resp, err := d.sendRequest("POST", "addEntity", body, queryArgs) + + if err != nil { + return err + } + defer resp.Body.Close() + + addTxtBytes, _ := ioutil.ReadAll(resp.Body) + addTxtResp := string(addTxtBytes) + // addEntity responds only with body text containing the ID of the created record + _, err = strconv.ParseUint(addTxtResp, 10, 64) + if err != nil { + return fmt.Errorf("Bluecat API addEntity request failed: %s", addTxtResp) + } + + err = d.deploy(uint(parentZoneId)) + if err != nil { + return err + } + + err = d.logout() + if err != nil { + return err + } + + return nil +} + +// Deploy the DNS config for the specified entity to the authoritative servers +func (d *DNSProvider) deploy(entityId uint) error { + queryArgs := map[string]string{ + "entityId": strconv.FormatUint(uint64(entityId), 10), + } + + resp, err := d.sendRequest("POST", "quickDeploy", nil, queryArgs) + + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + + err := d.login() + if err != nil { + return err + } + + viewId, err := d.lookupViewId(d.dnsView) + if err != nil { + return err + } + + parentId, name, err := d.lookupParentZoneId(viewId, fqdn) + if err != nil { + return err + } + + queryArgs := map[string]string{ + "parentId": strconv.FormatUint(uint64(parentId), 10), + "name": name, + "type": txtType, + } + + resp, err := d.sendRequest("GET", "getEntityByName", nil, queryArgs) + if err != nil { + return err + } + defer resp.Body.Close() + + var txtRec entityResponse + err = json.NewDecoder(resp.Body).Decode(&txtRec) + if err != nil { + return err + } + queryArgs = map[string]string{ + "objectId": strconv.FormatUint(uint64(txtRec.Id), 10), + } + + resp, err = d.sendRequest("DELETE", "delete", nil, queryArgs) + if err != nil { + return err + } + defer resp.Body.Close() + + err = d.deploy(parentId) + if err != nil { + return err + } + + err = d.logout() + if err != nil { + return err + } + + return nil +} + +//JSON body for Bluecat entity requests and responses +type bluecatEntity struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Properties string `json:"properties"` +} diff --git a/providers/dns/bluecat/bluecat_test.go b/providers/dns/bluecat/bluecat_test.go new file mode 100644 index 00000000..c1138ffc --- /dev/null +++ b/providers/dns/bluecat/bluecat_test.go @@ -0,0 +1,57 @@ +package bluecat + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "time" +) + +var ( + bluecatLiveTest bool + bluecatServer string + bluecatUserName string + bluecatPassword string + bluecatConfigName string + bluecatDnsView string + bluecatDomain string +) + +func init() { + bluecatServer = os.Getenv("BLUECAT_SERVER_URL") + bluecatUserName = os.Getenv("BLUECAT_USER_NAME") + bluecatPassword = os.Getenv("BLUECAT_PASSWORD") + bluecatDomain = os.Getenv("BLUECAT_DOMAIN") + bluecatConfigName = os.Getenv("BLUECAT_CONFIG_NAME") + bluecatDnsView = os.Getenv("BLUECAT_DNS_VIEW") + if len(bluecatServer) > 0 && len(bluecatDomain) > 0 && len(bluecatUserName) > 0 && len(bluecatPassword) > 0 && len(bluecatConfigName) > 0 && len(bluecatDnsView) > 0 { + bluecatLiveTest = true + } +} + +func TestLiveBluecatPresent(t *testing.T) { + if !bluecatLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.Present(bluecatDomain, "", "123d==") + assert.NoError(t, err) +} + +func TestLiveBluecatCleanUp(t *testing.T) { + if !bluecatLiveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 1) + + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.CleanUp(bluecatDomain, "", "123d==") + assert.NoError(t, err) +} diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 931e3a5b..e1052fcf 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -34,6 +34,7 @@ import ( "github.com/xenolf/lego/providers/dns/rfc2136" "github.com/xenolf/lego/providers/dns/route53" "github.com/xenolf/lego/providers/dns/vultr" + "github.com/xenolf/lego/providers/dns/bluecat" ) func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) { @@ -44,6 +45,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) provider, err = azure.NewDNSProvider() case "auroradns": provider, err = auroradns.NewDNSProvider() + case "bluecat": + provider, err = bluecat.NewDNSProvider() case "cloudflare": provider, err = cloudflare.NewDNSProvider() case "cloudxns": From 3c9be22bc07982ae08633712de135853cee1d8dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jefferson=20Gir=C3=A3o?= Date: Tue, 3 Apr 2018 16:22:13 +0200 Subject: [PATCH 09/12] Add Akamai FastDNS as DNS provider (#522) * Adding support to Akamai FastDNS as DNS provider * Adding fastdns to the list of dnsproviders --- providers/dns/dns_providers.go | 3 + providers/dns/fastdns/fastdns.go | 139 ++++++++++++++++++++++++++ providers/dns/fastdns/fastdns_test.go | 117 ++++++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 providers/dns/fastdns/fastdns.go create mode 100644 providers/dns/fastdns/fastdns_test.go diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index e1052fcf..72ab0d8c 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -17,6 +17,7 @@ import ( "github.com/xenolf/lego/providers/dns/dyn" "github.com/xenolf/lego/providers/dns/exec" "github.com/xenolf/lego/providers/dns/exoscale" + "github.com/xenolf/lego/providers/dns/fastdns" "github.com/xenolf/lego/providers/dns/gandi" "github.com/xenolf/lego/providers/dns/gandiv5" "github.com/xenolf/lego/providers/dns/glesys" @@ -63,6 +64,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) provider, err = duckdns.NewDNSProvider() case "dyn": provider, err = dyn.NewDNSProvider() + case "fastdns": + provider, err = fastdns.NewDNSProvider() case "exoscale": provider, err = exoscale.NewDNSProvider() case "gandi": diff --git a/providers/dns/fastdns/fastdns.go b/providers/dns/fastdns/fastdns.go new file mode 100644 index 00000000..dcbb93e5 --- /dev/null +++ b/providers/dns/fastdns/fastdns.go @@ -0,0 +1,139 @@ +package fastdns + +import ( + "fmt" + "os" + "reflect" + + configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v1" + "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" + "github.com/xenolf/lego/acme" +) + +// DNSProvider is an implementation of the acme.ChallengeProvider interface. +type DNSProvider struct { + config edgegrid.Config +} + +// NewDNSProvider uses the supplied environment variables to return a DNSProvider instance: +// AKAMAI_HOST, AKAMAI_CLIENT_TOKEN, AKAMAI_CLIENT_SECRET, AKAMAI_ACCESS_TOKEN +func NewDNSProvider() (*DNSProvider, error) { + host := os.Getenv("AKAMAI_HOST") + clientToken := os.Getenv("AKAMAI_CLIENT_TOKEN") + clientSecret := os.Getenv("AKAMAI_CLIENT_SECRET") + accessToken := os.Getenv("AKAMAI_ACCESS_TOKEN") + + return NewDNSProviderClient(host, clientToken, clientSecret, accessToken) +} + +// NewDNSProviderClient uses the supplied parameters to return a DNSProvider instance +// configured for FastDNS. +func NewDNSProviderClient(host, clientToken, clientSecret, accessToken string) (*DNSProvider, error) { + if clientToken == "" || clientSecret == "" || accessToken == "" || host == "" { + return nil, fmt.Errorf("Akamai FastDNS credentials missing") + } + config := edgegrid.Config{ + Host: host, + ClientToken: clientToken, + ClientSecret: clientSecret, + AccessToken: accessToken, + MaxBody: 131072, + } + + return &DNSProvider{ + config: config, + }, nil +} + +// Present creates a TXT record to fullfil the dns-01 challenge. +func (c *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) + zoneName, recordName, err := c.findZoneAndRecordName(fqdn, domain) + if err != nil { + return err + } + + configdns.Init(c.config) + + zone, err := configdns.GetZone(zoneName) + if err != nil { + return err + } + + record := configdns.NewTxtRecord() + record.SetField("name", recordName) + record.SetField("ttl", ttl) + record.SetField("target", value) + record.SetField("active", true) + + existingRecord := c.findExistingRecord(zone, recordName) + + if existingRecord != nil { + if reflect.DeepEqual(existingRecord.ToMap(), record.ToMap()) { + return nil + } + zone.RemoveRecord(existingRecord) + return c.createRecord(zone, record) + } + + return c.createRecord(zone, record) +} + +// CleanUp removes the record matching the specified parameters. +func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + zoneName, recordName, err := c.findZoneAndRecordName(fqdn, domain) + if err != nil { + return err + } + + configdns.Init(c.config) + + zone, err := configdns.GetZone(zoneName) + if err != nil { + return err + } + + existingRecord := c.findExistingRecord(zone, recordName) + + if existingRecord != nil { + err := zone.RemoveRecord(existingRecord) + if err != nil { + return err + } + return zone.Save() + } + + return nil +} + +func (c *DNSProvider) findZoneAndRecordName(fqdn, domain string) (string, string, error) { + zone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + if err != nil { + return "", "", err + } + zone = acme.UnFqdn(zone) + name := acme.UnFqdn(fqdn) + name = name[:len(name)-len("."+zone)] + + return zone, name, nil +} + +func (c *DNSProvider) findExistingRecord(zone *configdns.Zone, recordName string) *configdns.TxtRecord { + for _, r := range zone.Zone.Txt { + if r.Name == recordName { + return r + } + } + + return nil +} + +func (c *DNSProvider) createRecord(zone *configdns.Zone, record *configdns.TxtRecord) error { + err := zone.AddRecord(record) + if err != nil { + return err + } + + return zone.Save() +} diff --git a/providers/dns/fastdns/fastdns_test.go b/providers/dns/fastdns/fastdns_test.go new file mode 100644 index 00000000..2c36f614 --- /dev/null +++ b/providers/dns/fastdns/fastdns_test.go @@ -0,0 +1,117 @@ +package fastdns + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + fastdnsLiveTest bool + host string + clientToken string + clientSecret string + accessToken string + testDomain string +) + +func init() { + host = os.Getenv("AKAMAI_HOST") + clientToken = os.Getenv("AKAMAI_CLIENT_TOKEN") + clientSecret = os.Getenv("AKAMAI_CLIENT_SECRET") + accessToken = os.Getenv("AKAMAI_ACCESS_TOKEN") + testDomain = os.Getenv("AKAMAI_TEST_DOMAIN") + + if len(host) > 0 && len(clientToken) > 0 && len(clientSecret) > 0 && len(accessToken) > 0 { + fastdnsLiveTest = true + } +} + +func restoreFastdnsEnv() { + os.Setenv("AKAMAI_HOST", host) + os.Setenv("AKAMAI_CLIENT_TOKEN", clientToken) + os.Setenv("AKAMAI_CLIENT_SECRET", clientSecret) + os.Setenv("AKAMAI_ACCESS_TOKEN", accessToken) +} + +func TestNewDNSProviderValid(t *testing.T) { + os.Setenv("AKAMAI_HOST", "") + os.Setenv("AKAMAI_CLIENT_TOKEN", "") + os.Setenv("AKAMAI_CLIENT_SECRET", "") + os.Setenv("AKAMAI_ACCESS_TOKEN", "") + _, err := NewDNSProviderClient("somehost", "someclienttoken", "someclientsecret", "someaccesstoken") + assert.NoError(t, err) + restoreFastdnsEnv() +} +func TestNewDNSProviderValidEnv(t *testing.T) { + os.Setenv("AKAMAI_HOST", "somehost") + os.Setenv("AKAMAI_CLIENT_TOKEN", "someclienttoken") + os.Setenv("AKAMAI_CLIENT_SECRET", "someclientsecret") + os.Setenv("AKAMAI_ACCESS_TOKEN", "someaccesstoken") + _, err := NewDNSProvider() + assert.NoError(t, err) + restoreFastdnsEnv() +} + +func TestNewDNSProviderMissingCredErr(t *testing.T) { + os.Setenv("AKAMAI_HOST", "") + os.Setenv("AKAMAI_CLIENT_TOKEN", "") + os.Setenv("AKAMAI_CLIENT_SECRET", "") + os.Setenv("AKAMAI_ACCESS_TOKEN", "") + + _, err := NewDNSProvider() + assert.EqualError(t, err, "Akamai FastDNS credentials missing") + restoreFastdnsEnv() +} + +func TestLiveFastdnsPresent(t *testing.T) { + if !fastdnsLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProviderClient(host, clientToken, clientSecret, accessToken) + assert.NoError(t, err) + + err = provider.Present(testDomain, "", "123d==") + assert.NoError(t, err) + + // Present Twice to handle create / update + err = provider.Present(testDomain, "", "123d==") + assert.NoError(t, err) +} + +func TestExtractRootRecordName(t *testing.T) { + provider, err := NewDNSProviderClient("somehost", "someclienttoken", "someclientsecret", "someaccesstoken") + assert.NoError(t, err) + + zone, recordName, err := provider.findZoneAndRecordName("_acme-challenge.bar.com.", "bar.com") + assert.NoError(t, err) + assert.Equal(t, "bar.com", zone) + assert.Equal(t, "_acme-challenge", recordName) +} + +func TestExtractSubRecordName(t *testing.T) { + provider, err := NewDNSProviderClient("somehost", "someclienttoken", "someclientsecret", "someaccesstoken") + assert.NoError(t, err) + + zone, recordName, err := provider.findZoneAndRecordName("_acme-challenge.foo.bar.com.", "foo.bar.com") + assert.NoError(t, err) + assert.Equal(t, "bar.com", zone) + assert.Equal(t, "_acme-challenge.foo", recordName) +} + +func TestLiveFastdnsCleanUp(t *testing.T) { + if !fastdnsLiveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 1) + + provider, err := NewDNSProviderClient(host, clientToken, clientSecret, accessToken) + assert.NoError(t, err) + + err = provider.CleanUp(testDomain, "", "123d==") + assert.NoError(t, err) +} From b2c4f3c84edb8fa3cdf4ae7d76acc5c57798de94 Mon Sep 17 00:00:00 2001 From: Johannes Ebke Date: Thu, 12 Apr 2018 15:08:23 +0200 Subject: [PATCH 10/12] route53: Use NewSessionWithOptions instead of deprecated New. Fixes #458. (#528) --- providers/dns/route53/route53.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/providers/dns/route53/route53.go b/providers/dns/route53/route53.go index 934f0a2d..e16e12f0 100644 --- a/providers/dns/route53/route53.go +++ b/providers/dns/route53/route53.go @@ -70,7 +70,11 @@ func NewDNSProvider() (*DNSProvider, error) { r := customRetryer{} r.NumMaxRetries = maxRetries config := request.WithRetryer(aws.NewConfig(), r) - client := route53.New(session.New(config)) + session, err := session.NewSessionWithOptions(session.Options{Config: *config}) + if err != nil { + return nil, err + } + client := route53.New(session) return &DNSProvider{ client: client, From 5922ca92694ae0d455fc9de4776aa4f141c0dcb6 Mon Sep 17 00:00:00 2001 From: dajenet Date: Sun, 15 Apr 2018 15:49:13 +0200 Subject: [PATCH 11/12] Fix dnsimple api (#529) --- providers/dns/dnsimple/dnsimple.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/dns/dnsimple/dnsimple.go b/providers/dns/dnsimple/dnsimple.go index e3fea79e..df76a241 100644 --- a/providers/dns/dnsimple/dnsimple.go +++ b/providers/dns/dnsimple/dnsimple.go @@ -176,5 +176,5 @@ func (c *DNSProvider) getAccountID() (string, error) { return "", fmt.Errorf("DNSimple user tokens are not supported, please use an account token.") } - return strconv.Itoa(whoamiResponse.Data.Account.ID), nil + return strconv.FormatInt(whoamiResponse.Data.Account.ID, 10), nil } From 8e9c5ac3e6bf1392a581eece36752e7360a3414b Mon Sep 17 00:00:00 2001 From: Daniel Alan Miller Date: Thu, 26 Apr 2018 01:12:41 +1000 Subject: [PATCH 12/12] Adding output of which envvars are missing in Cloudflare and Azure (#537) * Adding output of which envvars are missing in Cloudflare dns provider * go fmt, duh * Fixing & adding test(s) * Adding azure missing env vars checking * Fixing test * Doh, fixing up expected output --- providers/dns/azure/azure.go | 12 +++++++++--- providers/dns/azure/azure_test.go | 2 +- providers/dns/cloudflare/cloudflare.go | 10 +++++++++- providers/dns/cloudflare/cloudflare_test.go | 9 ++++++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/providers/dns/azure/azure.go b/providers/dns/azure/azure.go index cc15ca7e..9022af47 100644 --- a/providers/dns/azure/azure.go +++ b/providers/dns/azure/azure.go @@ -28,7 +28,7 @@ type DNSProvider struct { tenantId string resourceGroup string - context context.Context + context context.Context } // NewDNSProvider returns a DNSProvider instance configured for azure. @@ -47,7 +47,13 @@ func NewDNSProvider() (*DNSProvider, error) { // DNSProvider instance configured for azure. func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup string) (*DNSProvider, error) { if clientId == "" || clientSecret == "" || subscriptionId == "" || tenantId == "" || resourceGroup == "" { - return nil, fmt.Errorf("Azure configuration missing") + missingEnvVars := []string{} + for _, envVar := range []string{"AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_SUBSCRIPTION_ID", "AZURE_TENANT_ID", "AZURE_RESOURCE_GROUP"} { + if os.Getenv(envVar) == "" { + missingEnvVars = append(missingEnvVars, envVar) + } + } + return nil, fmt.Errorf("Azure configuration missing: %s", strings.Join(missingEnvVars, ",")) } return &DNSProvider{ @@ -57,7 +63,7 @@ func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, tenantId: tenantId, resourceGroup: resourceGroup, // TODO: A timeout can be added here for cancellation purposes. - context: context.Background(), + context: context.Background(), }, nil } diff --git a/providers/dns/azure/azure_test.go b/providers/dns/azure/azure_test.go index db55f578..3eeb10fc 100644 --- a/providers/dns/azure/azure_test.go +++ b/providers/dns/azure/azure_test.go @@ -58,7 +58,7 @@ func TestNewDNSProviderValidEnv(t *testing.T) { func TestNewDNSProviderMissingCredErr(t *testing.T) { os.Setenv("AZURE_SUBSCRIPTION_ID", "") _, err := NewDNSProvider() - assert.EqualError(t, err, "Azure configuration missing") + assert.EqualError(t, err, "Azure configuration missing: AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID,AZURE_RESOURCE_GROUP") restoreAzureEnv() } diff --git a/providers/dns/cloudflare/cloudflare.go b/providers/dns/cloudflare/cloudflare.go index 84952238..d62b26f0 100644 --- a/providers/dns/cloudflare/cloudflare.go +++ b/providers/dns/cloudflare/cloudflare.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "os" + "strings" "time" "github.com/xenolf/lego/acme" @@ -37,7 +38,14 @@ func NewDNSProvider() (*DNSProvider, error) { // DNSProvider instance configured for cloudflare. func NewDNSProviderCredentials(email, key string) (*DNSProvider, error) { if email == "" || key == "" { - return nil, fmt.Errorf("CloudFlare credentials missing") + missingEnvVars := []string{} + if email == "" { + missingEnvVars = append(missingEnvVars, "CLOUDFLARE_EMAIL") + } + if key == "" { + missingEnvVars = append(missingEnvVars, "CLOUDFLARE_API_KEY") + } + return nil, fmt.Errorf("CloudFlare credentials missing: %s", strings.Join(missingEnvVars, ",")) } return &DNSProvider{ diff --git a/providers/dns/cloudflare/cloudflare_test.go b/providers/dns/cloudflare/cloudflare_test.go index 19b5a40b..9fab1622 100644 --- a/providers/dns/cloudflare/cloudflare_test.go +++ b/providers/dns/cloudflare/cloudflare_test.go @@ -49,10 +49,17 @@ func TestNewDNSProviderMissingCredErr(t *testing.T) { os.Setenv("CLOUDFLARE_EMAIL", "") os.Setenv("CLOUDFLARE_API_KEY", "") _, err := NewDNSProvider() - assert.EqualError(t, err, "CloudFlare credentials missing") + assert.EqualError(t, err, "CloudFlare credentials missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY") restoreCloudFlareEnv() } +func TestNewDNSProviderMissingCredErrSingle(t *testing.T){ + os.Setenv("CLOUDFLARE_EMAIL", "awesome@possum.com") + _, err:= NewDNSProvider() + assert.EqualError(t, err, "CloudFlare credentials missing: CLOUDFLARE_API_KEY") + restoreCloudFlareEnv() +} + func TestCloudFlarePresent(t *testing.T) { if !cflareLiveTest { t.Skip("skipping live test")