summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/xenolf/lego/providers/dns
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/xenolf/lego/providers/dns')
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go223
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare_test.go80
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean.go166
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean_test.go117
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple.go141
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple_test.go79
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy.go248
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go37
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dyn/dyn.go274
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/dyn/dyn_test.go53
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/gandi/gandi.go472
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/gandi/gandi_test.go939
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go158
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud_test.go85
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/linode/linode.go131
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/linode/linode_test.go317
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap.go416
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap_test.go402
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/ovh/ovh.go159
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/ovh/ovh_test.go103
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/pdns/README.md7
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/pdns/pdns.go343
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/pdns/pdns_test.go80
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136.go129
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136_test.go244
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/route53/fixtures_test.go39
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/route53/route53.go171
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/route53/route53_integration_test.go70
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/route53/route53_test.go87
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/route53/testutil_test.go38
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/vultr/vultr.go127
-rw-r--r--vendor/github.com/xenolf/lego/providers/dns/vultr/vultr_test.go65
32 files changed, 6000 insertions, 0 deletions
diff --git a/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go b/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go
new file mode 100644
index 000000000..84952238d
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare.go
@@ -0,0 +1,223 @@
+// Package cloudflare implements a DNS provider for solving the DNS-01
+// challenge using cloudflare DNS.
+package cloudflare
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// CloudFlareAPIURL represents the API endpoint to call.
+// TODO: Unexport?
+const CloudFlareAPIURL = "https://api.cloudflare.com/client/v4"
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface
+type DNSProvider struct {
+ authEmail string
+ authKey string
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for cloudflare.
+// Credentials must be passed in the environment variables: CLOUDFLARE_EMAIL
+// and CLOUDFLARE_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ email := os.Getenv("CLOUDFLARE_EMAIL")
+ key := os.Getenv("CLOUDFLARE_API_KEY")
+ return NewDNSProviderCredentials(email, key)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for cloudflare.
+func NewDNSProviderCredentials(email, key string) (*DNSProvider, error) {
+ if email == "" || key == "" {
+ return nil, fmt.Errorf("CloudFlare credentials missing")
+ }
+
+ return &DNSProvider{
+ authEmail: email,
+ authKey: key,
+ }, nil
+}
+
+// Timeout returns the timeout and interval to use when checking for DNS
+// propagation. Adjusting here to cope with spikes in propagation times.
+func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return 120 * time.Second, 2 * time.Second
+}
+
+// Present creates a TXT record to fulfil the dns-01 challenge
+func (c *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
+ zoneID, err := c.getHostedZoneID(fqdn)
+ if err != nil {
+ return err
+ }
+
+ rec := cloudFlareRecord{
+ Type: "TXT",
+ Name: acme.UnFqdn(fqdn),
+ Content: value,
+ TTL: 120,
+ }
+
+ body, err := json.Marshal(rec)
+ if err != nil {
+ return err
+ }
+
+ _, err = c.makeRequest("POST", fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body))
+ if err != nil {
+ 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)
+
+ record, err := c.findTxtRecord(fqdn)
+ if err != nil {
+ return err
+ }
+
+ _, err = c.makeRequest("DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
+ // HostedZone represents a CloudFlare DNS zone
+ type HostedZone struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ }
+
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return "", err
+ }
+
+ result, err := c.makeRequest("GET", "/zones?name="+acme.UnFqdn(authZone), nil)
+ if err != nil {
+ return "", err
+ }
+
+ var hostedZone []HostedZone
+ err = json.Unmarshal(result, &hostedZone)
+ if err != nil {
+ return "", err
+ }
+
+ if len(hostedZone) != 1 {
+ return "", fmt.Errorf("Zone %s not found in CloudFlare for domain %s", authZone, fqdn)
+ }
+
+ return hostedZone[0].ID, nil
+}
+
+func (c *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) {
+ zoneID, err := c.getHostedZoneID(fqdn)
+ if err != nil {
+ return nil, err
+ }
+
+ result, err := c.makeRequest(
+ "GET",
+ fmt.Sprintf("/zones/%s/dns_records?per_page=1000&type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)),
+ nil,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ var records []cloudFlareRecord
+ err = json.Unmarshal(result, &records)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, rec := range records {
+ if rec.Name == acme.UnFqdn(fqdn) {
+ return &rec, nil
+ }
+ }
+
+ return nil, fmt.Errorf("No existing record found for %s", fqdn)
+}
+
+func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
+ // APIError contains error details for failed requests
+ type APIError struct {
+ Code int `json:"code,omitempty"`
+ Message string `json:"message,omitempty"`
+ ErrorChain []APIError `json:"error_chain,omitempty"`
+ }
+
+ // APIResponse represents a response from CloudFlare API
+ type APIResponse struct {
+ Success bool `json:"success"`
+ Errors []*APIError `json:"errors"`
+ Result json.RawMessage `json:"result"`
+ }
+
+ req, err := http.NewRequest(method, fmt.Sprintf("%s%s", CloudFlareAPIURL, uri), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("X-Auth-Email", c.authEmail)
+ req.Header.Set("X-Auth-Key", c.authKey)
+ //req.Header.Set("User-Agent", userAgent())
+
+ client := http.Client{Timeout: 30 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("Error querying Cloudflare API -> %v", err)
+ }
+
+ defer resp.Body.Close()
+
+ var r APIResponse
+ err = json.NewDecoder(resp.Body).Decode(&r)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.Success {
+ if len(r.Errors) > 0 {
+ errStr := ""
+ for _, apiErr := range r.Errors {
+ errStr += fmt.Sprintf("\t Error: %d: %s", apiErr.Code, apiErr.Message)
+ for _, chainErr := range apiErr.ErrorChain {
+ errStr += fmt.Sprintf("<- %d: %s", chainErr.Code, chainErr.Message)
+ }
+ }
+ return nil, fmt.Errorf("Cloudflare API Error \n%s", errStr)
+ }
+ return nil, fmt.Errorf("Cloudflare API error")
+ }
+
+ return r.Result, nil
+}
+
+// cloudFlareRecord represents a CloudFlare DNS record
+type cloudFlareRecord struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Content string `json:"content"`
+ ID string `json:"id,omitempty"`
+ TTL int `json:"ttl,omitempty"`
+ ZoneID string `json:"zone_id,omitempty"`
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare_test.go b/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare_test.go
new file mode 100644
index 000000000..19b5a40b9
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/cloudflare/cloudflare_test.go
@@ -0,0 +1,80 @@
+package cloudflare
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ cflareLiveTest bool
+ cflareEmail string
+ cflareAPIKey string
+ cflareDomain string
+)
+
+func init() {
+ cflareEmail = os.Getenv("CLOUDFLARE_EMAIL")
+ cflareAPIKey = os.Getenv("CLOUDFLARE_API_KEY")
+ cflareDomain = os.Getenv("CLOUDFLARE_DOMAIN")
+ if len(cflareEmail) > 0 && len(cflareAPIKey) > 0 && len(cflareDomain) > 0 {
+ cflareLiveTest = true
+ }
+}
+
+func restoreCloudFlareEnv() {
+ os.Setenv("CLOUDFLARE_EMAIL", cflareEmail)
+ os.Setenv("CLOUDFLARE_API_KEY", cflareAPIKey)
+}
+
+func TestNewDNSProviderValid(t *testing.T) {
+ os.Setenv("CLOUDFLARE_EMAIL", "")
+ os.Setenv("CLOUDFLARE_API_KEY", "")
+ _, err := NewDNSProviderCredentials("123", "123")
+ assert.NoError(t, err)
+ restoreCloudFlareEnv()
+}
+
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ os.Setenv("CLOUDFLARE_EMAIL", "test@example.com")
+ os.Setenv("CLOUDFLARE_API_KEY", "123")
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+ restoreCloudFlareEnv()
+}
+
+func TestNewDNSProviderMissingCredErr(t *testing.T) {
+ os.Setenv("CLOUDFLARE_EMAIL", "")
+ os.Setenv("CLOUDFLARE_API_KEY", "")
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "CloudFlare credentials missing")
+ restoreCloudFlareEnv()
+}
+
+func TestCloudFlarePresent(t *testing.T) {
+ if !cflareLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProviderCredentials(cflareEmail, cflareAPIKey)
+ assert.NoError(t, err)
+
+ err = provider.Present(cflareDomain, "", "123d==")
+ assert.NoError(t, err)
+}
+
+func TestCloudFlareCleanUp(t *testing.T) {
+ if !cflareLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ time.Sleep(time.Second * 2)
+
+ provider, err := NewDNSProviderCredentials(cflareEmail, cflareAPIKey)
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(cflareDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean.go b/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean.go
new file mode 100644
index 000000000..da261b39a
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean.go
@@ -0,0 +1,166 @@
+// Package digitalocean implements a DNS provider for solving the DNS-01
+// challenge using digitalocean DNS.
+package digitalocean
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface
+// that uses DigitalOcean's REST API to manage TXT records for a domain.
+type DNSProvider struct {
+ apiAuthToken string
+ recordIDs map[string]int
+ recordIDsMu sync.Mutex
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Digital
+// Ocean. Credentials must be passed in the environment variable:
+// DO_AUTH_TOKEN.
+func NewDNSProvider() (*DNSProvider, error) {
+ apiAuthToken := os.Getenv("DO_AUTH_TOKEN")
+ return NewDNSProviderCredentials(apiAuthToken)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for Digital Ocean.
+func NewDNSProviderCredentials(apiAuthToken string) (*DNSProvider, error) {
+ if apiAuthToken == "" {
+ return nil, fmt.Errorf("DigitalOcean credentials missing")
+ }
+ return &DNSProvider{
+ apiAuthToken: apiAuthToken,
+ recordIDs: make(map[string]int),
+ }, nil
+}
+
+// Present creates a TXT record using the specified parameters
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ // txtRecordRequest represents the request body to DO's API to make a TXT record
+ type txtRecordRequest struct {
+ RecordType string `json:"type"`
+ Name string `json:"name"`
+ Data string `json:"data"`
+ }
+
+ // txtRecordResponse represents a response from DO's API after making a TXT record
+ type txtRecordResponse struct {
+ DomainRecord struct {
+ ID int `json:"id"`
+ Type string `json:"type"`
+ Name string `json:"name"`
+ Data string `json:"data"`
+ } `json:"domain_record"`
+ }
+
+ fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
+
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
+ }
+
+ authZone = acme.UnFqdn(authZone)
+
+ reqURL := fmt.Sprintf("%s/v2/domains/%s/records", digitalOceanBaseURL, authZone)
+ reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value}
+ body, err := json.Marshal(reqData)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest("POST", reqURL, bytes.NewReader(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.apiAuthToken))
+
+ client := http.Client{Timeout: 30 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 400 {
+ var errInfo digitalOceanAPIError
+ json.NewDecoder(resp.Body).Decode(&errInfo)
+ return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message)
+ }
+
+ // Everything looks good; but we'll need the ID later to delete the record
+ var respData txtRecordResponse
+ err = json.NewDecoder(resp.Body).Decode(&respData)
+ if err != nil {
+ return err
+ }
+ d.recordIDsMu.Lock()
+ d.recordIDs[fqdn] = respData.DomainRecord.ID
+ d.recordIDsMu.Unlock()
+
+ 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)
+
+ // get the record's unique ID from when we created it
+ d.recordIDsMu.Lock()
+ recordID, ok := d.recordIDs[fqdn]
+ d.recordIDsMu.Unlock()
+ if !ok {
+ return fmt.Errorf("unknown record ID for '%s'", fqdn)
+ }
+
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
+ }
+
+ authZone = acme.UnFqdn(authZone)
+
+ reqURL := fmt.Sprintf("%s/v2/domains/%s/records/%d", digitalOceanBaseURL, authZone, recordID)
+ req, err := http.NewRequest("DELETE", reqURL, nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.apiAuthToken))
+
+ client := http.Client{Timeout: 30 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 400 {
+ var errInfo digitalOceanAPIError
+ json.NewDecoder(resp.Body).Decode(&errInfo)
+ return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message)
+ }
+
+ // Delete record ID from map
+ d.recordIDsMu.Lock()
+ delete(d.recordIDs, fqdn)
+ d.recordIDsMu.Unlock()
+
+ return nil
+}
+
+type digitalOceanAPIError struct {
+ ID string `json:"id"`
+ Message string `json:"message"`
+}
+
+var digitalOceanBaseURL = "https://api.digitalocean.com"
diff --git a/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean_test.go b/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean_test.go
new file mode 100644
index 000000000..7498508ba
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/digitalocean/digitalocean_test.go
@@ -0,0 +1,117 @@
+package digitalocean
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+var fakeDigitalOceanAuth = "asdf1234"
+
+func TestDigitalOceanPresent(t *testing.T) {
+ var requestReceived bool
+
+ mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ requestReceived = true
+
+ if got, want := r.Method, "POST"; got != want {
+ t.Errorf("Expected method to be '%s' but got '%s'", want, got)
+ }
+ if got, want := r.URL.Path, "/v2/domains/example.com/records"; got != want {
+ t.Errorf("Expected path to be '%s' but got '%s'", want, got)
+ }
+ if got, want := r.Header.Get("Content-Type"), "application/json"; got != want {
+ t.Errorf("Expected Content-Type to be '%s' but got '%s'", want, got)
+ }
+ if got, want := r.Header.Get("Authorization"), "Bearer asdf1234"; got != want {
+ t.Errorf("Expected Authorization to be '%s' but got '%s'", want, got)
+ }
+
+ reqBody, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ t.Fatalf("Error reading request body: %v", err)
+ }
+ if got, want := string(reqBody), `{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI"}`; got != want {
+ t.Errorf("Expected body data to be: `%s` but got `%s`", want, got)
+ }
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, `{
+ "domain_record": {
+ "id": 1234567,
+ "type": "TXT",
+ "name": "_acme-challenge",
+ "data": "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI",
+ "priority": null,
+ "port": null,
+ "weight": null
+ }
+ }`)
+ }))
+ defer mock.Close()
+ digitalOceanBaseURL = mock.URL
+
+ doprov, err := NewDNSProviderCredentials(fakeDigitalOceanAuth)
+ if doprov == nil {
+ t.Fatal("Expected non-nil DigitalOcean provider, but was nil")
+ }
+ if err != nil {
+ t.Fatalf("Expected no error creating provider, but got: %v", err)
+ }
+
+ err = doprov.Present("example.com", "", "foobar")
+ if err != nil {
+ t.Fatalf("Expected no error creating TXT record, but got: %v", err)
+ }
+ if !requestReceived {
+ t.Error("Expected request to be received by mock backend, but it wasn't")
+ }
+}
+
+func TestDigitalOceanCleanUp(t *testing.T) {
+ var requestReceived bool
+
+ mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ requestReceived = true
+
+ if got, want := r.Method, "DELETE"; got != want {
+ t.Errorf("Expected method to be '%s' but got '%s'", want, got)
+ }
+ if got, want := r.URL.Path, "/v2/domains/example.com/records/1234567"; got != want {
+ t.Errorf("Expected path to be '%s' but got '%s'", want, got)
+ }
+ // NOTE: Even though the body is empty, DigitalOcean API docs still show setting this Content-Type...
+ if got, want := r.Header.Get("Content-Type"), "application/json"; got != want {
+ t.Errorf("Expected Content-Type to be '%s' but got '%s'", want, got)
+ }
+ if got, want := r.Header.Get("Authorization"), "Bearer asdf1234"; got != want {
+ t.Errorf("Expected Authorization to be '%s' but got '%s'", want, got)
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+ }))
+ defer mock.Close()
+ digitalOceanBaseURL = mock.URL
+
+ doprov, err := NewDNSProviderCredentials(fakeDigitalOceanAuth)
+ if doprov == nil {
+ t.Fatal("Expected non-nil DigitalOcean provider, but was nil")
+ }
+ if err != nil {
+ t.Fatalf("Expected no error creating provider, but got: %v", err)
+ }
+
+ doprov.recordIDsMu.Lock()
+ doprov.recordIDs["_acme-challenge.example.com."] = 1234567
+ doprov.recordIDsMu.Unlock()
+
+ err = doprov.CleanUp("example.com", "", "")
+ if err != nil {
+ t.Fatalf("Expected no error removing TXT record, but got: %v", err)
+ }
+ if !requestReceived {
+ t.Error("Expected request to be received by mock backend, but it wasn't")
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple.go b/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple.go
new file mode 100644
index 000000000..c903a35ce
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple.go
@@ -0,0 +1,141 @@
+// Package dnsimple implements a DNS provider for solving the DNS-01 challenge
+// using dnsimple DNS.
+package dnsimple
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/weppos/dnsimple-go/dnsimple"
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface.
+type DNSProvider struct {
+ client *dnsimple.Client
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for dnsimple.
+// Credentials must be passed in the environment variables: DNSIMPLE_EMAIL
+// and DNSIMPLE_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ email := os.Getenv("DNSIMPLE_EMAIL")
+ key := os.Getenv("DNSIMPLE_API_KEY")
+ return NewDNSProviderCredentials(email, key)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for dnsimple.
+func NewDNSProviderCredentials(email, key string) (*DNSProvider, error) {
+ if email == "" || key == "" {
+ return nil, fmt.Errorf("DNSimple credentials missing")
+ }
+
+ return &DNSProvider{
+ client: dnsimple.NewClient(key, email),
+ }, 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)
+
+ zoneID, zoneName, err := c.getHostedZone(domain)
+ if err != nil {
+ return err
+ }
+
+ recordAttributes := c.newTxtRecord(zoneName, fqdn, value, ttl)
+ _, _, err = c.client.Domains.CreateRecord(zoneID, *recordAttributes)
+ if err != nil {
+ return fmt.Errorf("DNSimple 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.findTxtRecords(domain, fqdn)
+ if err != nil {
+ return err
+ }
+
+ for _, rec := range records {
+ _, err := c.client.Domains.DeleteRecord(rec.DomainId, rec.Id)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (c *DNSProvider) getHostedZone(domain string) (string, string, error) {
+ zones, _, err := c.client.Domains.List()
+ if err != nil {
+ return "", "", fmt.Errorf("DNSimple API call failed: %v", err)
+ }
+
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return "", "", err
+ }
+
+ var hostedZone dnsimple.Domain
+ for _, zone := range zones {
+ if zone.Name == acme.UnFqdn(authZone) {
+ hostedZone = zone
+ }
+ }
+
+ if hostedZone.Id == 0 {
+ return "", "", fmt.Errorf("Zone %s not found in DNSimple for domain %s", authZone, domain)
+
+ }
+
+ return fmt.Sprintf("%v", hostedZone.Id), hostedZone.Name, nil
+}
+
+func (c *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnsimple.Record, error) {
+ zoneID, zoneName, err := c.getHostedZone(domain)
+ if err != nil {
+ return nil, err
+ }
+
+ var records []dnsimple.Record
+ result, _, err := c.client.Domains.ListRecords(zoneID, "", "TXT")
+ if err != nil {
+ return records, fmt.Errorf("DNSimple API call has failed: %v", err)
+ }
+
+ recordName := c.extractRecordName(fqdn, zoneName)
+ for _, record := range result {
+ if record.Name == recordName {
+ records = append(records, record)
+ }
+ }
+
+ return records, nil
+}
+
+func (c *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) *dnsimple.Record {
+ name := c.extractRecordName(fqdn, zone)
+
+ return &dnsimple.Record{
+ Type: "TXT",
+ Name: name,
+ Content: value,
+ TTL: ttl,
+ }
+}
+
+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/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple_test.go b/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple_test.go
new file mode 100644
index 000000000..4926b3df9
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dnsimple/dnsimple_test.go
@@ -0,0 +1,79 @@
+package dnsimple
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ dnsimpleLiveTest bool
+ dnsimpleEmail string
+ dnsimpleAPIKey string
+ dnsimpleDomain string
+)
+
+func init() {
+ dnsimpleEmail = os.Getenv("DNSIMPLE_EMAIL")
+ dnsimpleAPIKey = os.Getenv("DNSIMPLE_API_KEY")
+ dnsimpleDomain = os.Getenv("DNSIMPLE_DOMAIN")
+ if len(dnsimpleEmail) > 0 && len(dnsimpleAPIKey) > 0 && len(dnsimpleDomain) > 0 {
+ dnsimpleLiveTest = true
+ }
+}
+
+func restoreDNSimpleEnv() {
+ os.Setenv("DNSIMPLE_EMAIL", dnsimpleEmail)
+ os.Setenv("DNSIMPLE_API_KEY", dnsimpleAPIKey)
+}
+
+func TestNewDNSProviderValid(t *testing.T) {
+ os.Setenv("DNSIMPLE_EMAIL", "")
+ os.Setenv("DNSIMPLE_API_KEY", "")
+ _, err := NewDNSProviderCredentials("example@example.com", "123")
+ assert.NoError(t, err)
+ restoreDNSimpleEnv()
+}
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ os.Setenv("DNSIMPLE_EMAIL", "example@example.com")
+ os.Setenv("DNSIMPLE_API_KEY", "123")
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+ restoreDNSimpleEnv()
+}
+
+func TestNewDNSProviderMissingCredErr(t *testing.T) {
+ os.Setenv("DNSIMPLE_EMAIL", "")
+ os.Setenv("DNSIMPLE_API_KEY", "")
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "DNSimple credentials missing")
+ restoreDNSimpleEnv()
+}
+
+func TestLiveDNSimplePresent(t *testing.T) {
+ if !dnsimpleLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProviderCredentials(dnsimpleEmail, dnsimpleAPIKey)
+ assert.NoError(t, err)
+
+ err = provider.Present(dnsimpleDomain, "", "123d==")
+ assert.NoError(t, err)
+}
+
+func TestLiveDNSimpleCleanUp(t *testing.T) {
+ if !dnsimpleLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ time.Sleep(time.Second * 1)
+
+ provider, err := NewDNSProviderCredentials(dnsimpleEmail, dnsimpleAPIKey)
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(dnsimpleDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy.go b/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy.go
new file mode 100644
index 000000000..c4363a4eb
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy.go
@@ -0,0 +1,248 @@
+package dnsmadeeasy
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha1"
+ "crypto/tls"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
+// DNSMadeEasy's DNS API to manage TXT records for a domain.
+type DNSProvider struct {
+ baseURL string
+ apiKey string
+ apiSecret string
+}
+
+// Domain holds the DNSMadeEasy API representation of a Domain
+type Domain struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+}
+
+// Record holds the DNSMadeEasy API representation of a Domain Record
+type Record struct {
+ ID int `json:"id"`
+ Type string `json:"type"`
+ Name string `json:"name"`
+ Value string `json:"value"`
+ TTL int `json:"ttl"`
+ SourceID int `json:"sourceId"`
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for DNSMadeEasy DNS.
+// Credentials must be passed in the environment variables: DNSMADEEASY_API_KEY
+// and DNSMADEEASY_API_SECRET.
+func NewDNSProvider() (*DNSProvider, error) {
+ dnsmadeeasyAPIKey := os.Getenv("DNSMADEEASY_API_KEY")
+ dnsmadeeasyAPISecret := os.Getenv("DNSMADEEASY_API_SECRET")
+ dnsmadeeasySandbox := os.Getenv("DNSMADEEASY_SANDBOX")
+
+ var baseURL string
+
+ sandbox, _ := strconv.ParseBool(dnsmadeeasySandbox)
+ if sandbox {
+ baseURL = "https://api.sandbox.dnsmadeeasy.com/V2.0"
+ } else {
+ baseURL = "https://api.dnsmadeeasy.com/V2.0"
+ }
+
+ return NewDNSProviderCredentials(baseURL, dnsmadeeasyAPIKey, dnsmadeeasyAPISecret)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for DNSMadeEasy.
+func NewDNSProviderCredentials(baseURL, apiKey, apiSecret string) (*DNSProvider, error) {
+ if baseURL == "" || apiKey == "" || apiSecret == "" {
+ return nil, fmt.Errorf("DNS Made Easy credentials missing")
+ }
+
+ return &DNSProvider{
+ baseURL: baseURL,
+ apiKey: apiKey,
+ apiSecret: apiSecret,
+ }, nil
+}
+
+// Present creates a TXT record using the specified parameters
+func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domainName, keyAuth)
+
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return err
+ }
+
+ // fetch the domain details
+ domain, err := d.getDomain(authZone)
+ if err != nil {
+ return err
+ }
+
+ // create the TXT record
+ name := strings.Replace(fqdn, "."+authZone, "", 1)
+ record := &Record{Type: "TXT", Name: name, Value: value, TTL: ttl}
+
+ err = d.createRecord(domain, record)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT records matching the specified parameters
+func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domainName, keyAuth)
+
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return err
+ }
+
+ // fetch the domain details
+ domain, err := d.getDomain(authZone)
+ if err != nil {
+ return err
+ }
+
+ // find matching records
+ name := strings.Replace(fqdn, "."+authZone, "", 1)
+ records, err := d.getRecords(domain, name, "TXT")
+ if err != nil {
+ return err
+ }
+
+ // delete records
+ for _, record := range *records {
+ err = d.deleteRecord(record)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (d *DNSProvider) getDomain(authZone string) (*Domain, error) {
+ domainName := authZone[0 : len(authZone)-1]
+ resource := fmt.Sprintf("%s%s", "/dns/managed/name?domainname=", domainName)
+
+ resp, err := d.sendRequest("GET", resource, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ domain := &Domain{}
+ err = json.NewDecoder(resp.Body).Decode(&domain)
+ if err != nil {
+ return nil, err
+ }
+
+ return domain, nil
+}
+
+func (d *DNSProvider) getRecords(domain *Domain, recordName, recordType string) (*[]Record, error) {
+ resource := fmt.Sprintf("%s/%d/%s%s%s%s", "/dns/managed", domain.ID, "records?recordName=", recordName, "&type=", recordType)
+
+ resp, err := d.sendRequest("GET", resource, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ type recordsResponse struct {
+ Records *[]Record `json:"data"`
+ }
+
+ records := &recordsResponse{}
+ err = json.NewDecoder(resp.Body).Decode(&records)
+ if err != nil {
+ return nil, err
+ }
+
+ return records.Records, nil
+}
+
+func (d *DNSProvider) createRecord(domain *Domain, record *Record) error {
+ url := fmt.Sprintf("%s/%d/%s", "/dns/managed", domain.ID, "records")
+
+ resp, err := d.sendRequest("POST", url, record)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
+
+func (d *DNSProvider) deleteRecord(record Record) error {
+ resource := fmt.Sprintf("%s/%d/%s/%d", "/dns/managed", record.SourceID, "records", record.ID)
+
+ resp, err := d.sendRequest("DELETE", resource, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
+
+func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*http.Response, error) {
+ url := fmt.Sprintf("%s%s", d.baseURL, resource)
+
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return nil, err
+ }
+
+ timestamp := time.Now().UTC().Format(time.RFC1123)
+ signature := computeHMAC(timestamp, d.apiSecret)
+
+ req, err := http.NewRequest(method, url, bytes.NewReader(body))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("x-dnsme-apiKey", d.apiKey)
+ req.Header.Set("x-dnsme-requestDate", timestamp)
+ req.Header.Set("x-dnsme-hmac", signature)
+ req.Header.Set("accept", "application/json")
+ req.Header.Set("content-type", "application/json")
+
+ transport := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ }
+ client := &http.Client{
+ Transport: transport,
+ Timeout: time.Duration(10 * time.Second),
+ }
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode > 299 {
+ return nil, fmt.Errorf("DNSMadeEasy API request failed with HTTP status code %d", resp.StatusCode)
+ }
+
+ return resp, nil
+}
+
+func computeHMAC(message string, secret string) string {
+ key := []byte(secret)
+ h := hmac.New(sha1.New, key)
+ h.Write([]byte(message))
+ return hex.EncodeToString(h.Sum(nil))
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go b/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go
new file mode 100644
index 000000000..e860ecb69
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go
@@ -0,0 +1,37 @@
+package dnsmadeeasy
+
+import (
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ testLive bool
+ testAPIKey string
+ testAPISecret string
+ testDomain string
+)
+
+func init() {
+ testAPIKey = os.Getenv("DNSMADEEASY_API_KEY")
+ testAPISecret = os.Getenv("DNSMADEEASY_API_SECRET")
+ testDomain = os.Getenv("DNSMADEEASY_DOMAIN")
+ os.Setenv("DNSMADEEASY_SANDBOX", "true")
+ testLive = len(testAPIKey) > 0 && len(testAPISecret) > 0
+}
+
+func TestPresentAndCleanup(t *testing.T) {
+ if !testLive {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProvider()
+
+ err = provider.Present(testDomain, "", "123d==")
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(testDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn.go b/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn.go
new file mode 100644
index 000000000..384bc850c
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn.go
@@ -0,0 +1,274 @@
+// Package dyn implements a DNS provider for solving the DNS-01 challenge
+// using Dyn Managed DNS.
+package dyn
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "strconv"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+var dynBaseURL = "https://api.dynect.net/REST"
+
+type dynResponse struct {
+ // One of 'success', 'failure', or 'incomplete'
+ Status string `json:"status"`
+
+ // The structure containing the actual results of the request
+ Data json.RawMessage `json:"data"`
+
+ // The ID of the job that was created in response to a request.
+ JobID int `json:"job_id"`
+
+ // A list of zero or more messages
+ Messages json.RawMessage `json:"msgs"`
+}
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
+// Dyn's Managed DNS API to manage TXT records for a domain.
+type DNSProvider struct {
+ customerName string
+ userName string
+ password string
+ token string
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS.
+// Credentials must be passed in the environment variables: DYN_CUSTOMER_NAME,
+// DYN_USER_NAME and DYN_PASSWORD.
+func NewDNSProvider() (*DNSProvider, error) {
+ customerName := os.Getenv("DYN_CUSTOMER_NAME")
+ userName := os.Getenv("DYN_USER_NAME")
+ password := os.Getenv("DYN_PASSWORD")
+ return NewDNSProviderCredentials(customerName, userName, password)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for Dyn DNS.
+func NewDNSProviderCredentials(customerName, userName, password string) (*DNSProvider, error) {
+ if customerName == "" || userName == "" || password == "" {
+ return nil, fmt.Errorf("DynDNS credentials missing")
+ }
+
+ return &DNSProvider{
+ customerName: customerName,
+ userName: userName,
+ password: password,
+ }, nil
+}
+
+func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*dynResponse, error) {
+ url := fmt.Sprintf("%s/%s", dynBaseURL, 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("Auth-Token", d.token)
+ }
+
+ 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("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
+ err = json.NewDecoder(resp.Body).Decode(&dynRes)
+ if err != nil {
+ return nil, err
+ }
+
+ if dynRes.Status == "failure" {
+ // TODO add better error handling
+ return nil, fmt.Errorf("Dyn API request failed: %s", dynRes.Messages)
+ }
+
+ return &dynRes, nil
+}
+
+// Starts a new Dyn API Session. Authenticates using customerName, userName,
+// password and receives a token to be used in for subsequent requests.
+func (d *DNSProvider) login() error {
+ type creds struct {
+ Customer string `json:"customer_name"`
+ User string `json:"user_name"`
+ Pass string `json:"password"`
+ }
+
+ type session struct {
+ Token string `json:"token"`
+ Version string `json:"version"`
+ }
+
+ payload := &creds{Customer: d.customerName, User: d.userName, Pass: d.password}
+ dynRes, err := d.sendRequest("POST", "Session", payload)
+ if err != nil {
+ return err
+ }
+
+ var s session
+ err = json.Unmarshal(dynRes.Data, &s)
+ if err != nil {
+ return err
+ }
+
+ d.token = s.Token
+
+ return nil
+}
+
+// Destroys Dyn Session
+func (d *DNSProvider) logout() error {
+ if len(d.token) == 0 {
+ // nothing to do
+ return nil
+ }
+
+ url := fmt.Sprintf("%s/Session", dynBaseURL)
+ req, err := http.NewRequest("DELETE", url, nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Auth-Token", d.token)
+
+ client := &http.Client{Timeout: time.Duration(10 * time.Second)}
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ return fmt.Errorf("Dyn API request failed to delete session with HTTP status code %d", resp.StatusCode)
+ }
+
+ d.token = ""
+
+ return 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)
+
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return err
+ }
+
+ err = d.login()
+ if err != nil {
+ return err
+ }
+
+ data := map[string]interface{}{
+ "rdata": map[string]string{
+ "txtdata": value,
+ },
+ "ttl": strconv.Itoa(ttl),
+ }
+
+ resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
+ _, err = d.sendRequest("POST", resource, data)
+ if err != nil {
+ return err
+ }
+
+ err = d.publish(authZone, "Added TXT record for ACME dns-01 challenge using lego client")
+ if err != nil {
+ return err
+ }
+
+ err = d.logout()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (d *DNSProvider) publish(zone, notes string) error {
+ type publish struct {
+ Publish bool `json:"publish"`
+ Notes string `json:"notes"`
+ }
+
+ pub := &publish{Publish: true, Notes: notes}
+ resource := fmt.Sprintf("Zone/%s/", zone)
+ _, err := d.sendRequest("PUT", resource, pub)
+ if err != nil {
+ return err
+ }
+
+ 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)
+
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return err
+ }
+
+ err = d.login()
+ if err != nil {
+ return err
+ }
+
+ resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn)
+ url := fmt.Sprintf("%s/%s", dynBaseURL, resource)
+ req, err := http.NewRequest("DELETE", url, nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Auth-Token", d.token)
+
+ client := &http.Client{Timeout: time.Duration(10 * time.Second)}
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ return fmt.Errorf("Dyn API request failed to delete TXT record HTTP status code %d", resp.StatusCode)
+ }
+
+ err = d.publish(authZone, "Removed TXT record for ACME dns-01 challenge using lego client")
+ if err != nil {
+ return err
+ }
+
+ err = d.logout()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn_test.go b/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn_test.go
new file mode 100644
index 000000000..0d28d5d0e
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/dyn/dyn_test.go
@@ -0,0 +1,53 @@
+package dyn
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ dynLiveTest bool
+ dynCustomerName string
+ dynUserName string
+ dynPassword string
+ dynDomain string
+)
+
+func init() {
+ dynCustomerName = os.Getenv("DYN_CUSTOMER_NAME")
+ dynUserName = os.Getenv("DYN_USER_NAME")
+ dynPassword = os.Getenv("DYN_PASSWORD")
+ dynDomain = os.Getenv("DYN_DOMAIN")
+ if len(dynCustomerName) > 0 && len(dynUserName) > 0 && len(dynPassword) > 0 && len(dynDomain) > 0 {
+ dynLiveTest = true
+ }
+}
+
+func TestLiveDynPresent(t *testing.T) {
+ if !dynLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ err = provider.Present(dynDomain, "", "123d==")
+ assert.NoError(t, err)
+}
+
+func TestLiveDynCleanUp(t *testing.T) {
+ if !dynLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ time.Sleep(time.Second * 1)
+
+ provider, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(dynDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi.go b/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi.go
new file mode 100644
index 000000000..422b02a21
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi.go
@@ -0,0 +1,472 @@
+// Package gandi implements a DNS provider for solving the DNS-01
+// challenge using Gandi DNS.
+package gandi
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// Gandi API reference: http://doc.rpc.gandi.net/index.html
+// Gandi API domain examples: http://doc.rpc.gandi.net/domain/faq.html
+
+var (
+ // endpoint is the Gandi XML-RPC endpoint used by Present and
+ // CleanUp. It is overridden during tests.
+ endpoint = "https://rpc.gandi.net/xmlrpc/"
+ // findZoneByFqdn determines the DNS zone of an fqdn. It is overridden
+ // during tests.
+ findZoneByFqdn = acme.FindZoneByFqdn
+)
+
+// inProgressInfo contains information about an in-progress challenge
+type inProgressInfo struct {
+ zoneID int // zoneID of gandi zone to restore in CleanUp
+ newZoneID int // zoneID of temporary gandi zone containing TXT record
+ authZone string // the domain name registered at gandi with trailing "."
+}
+
+// DNSProvider is an implementation of the
+// acme.ChallengeProviderTimeout interface that uses Gandi's XML-RPC
+// API to manage TXT records for a domain.
+type DNSProvider struct {
+ apiKey string
+ inProgressFQDNs map[string]inProgressInfo
+ inProgressAuthZones map[string]struct{}
+ inProgressMu sync.Mutex
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Gandi.
+// Credentials must be passed in the environment variable: GANDI_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ apiKey := os.Getenv("GANDI_API_KEY")
+ return NewDNSProviderCredentials(apiKey)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for Gandi.
+func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
+ if apiKey == "" {
+ return nil, fmt.Errorf("No Gandi API Key given")
+ }
+ return &DNSProvider{
+ apiKey: apiKey,
+ inProgressFQDNs: make(map[string]inProgressInfo),
+ inProgressAuthZones: make(map[string]struct{}),
+ }, nil
+}
+
+// Present creates a TXT record using the specified parameters. It
+// does this by creating and activating a new temporary Gandi DNS
+// zone. This new zone contains the TXT record.
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+ if ttl < 300 {
+ ttl = 300 // 300 is gandi minimum value for ttl
+ }
+ // find authZone and Gandi zone_id for fqdn
+ authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err)
+ }
+ zoneID, err := d.getZoneID(authZone)
+ if err != nil {
+ return err
+ }
+ // determine name of TXT record
+ if !strings.HasSuffix(
+ strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
+ return fmt.Errorf(
+ "Gandi 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()
+ if _, ok := d.inProgressAuthZones[authZone]; ok {
+ return fmt.Errorf(
+ "Gandi DNS: challenge already in progress for authZone %s",
+ authZone)
+ }
+ // perform API actions to create and activate new gandi zone
+ // containing the required TXT record
+ newZoneName := fmt.Sprintf(
+ "%s [ACME Challenge %s]",
+ acme.UnFqdn(authZone), time.Now().Format(time.RFC822Z))
+ newZoneID, err := d.cloneZone(zoneID, newZoneName)
+ if err != nil {
+ return err
+ }
+ newZoneVersion, err := d.newZoneVersion(newZoneID)
+ if err != nil {
+ return err
+ }
+ err = d.addTXTRecord(newZoneID, newZoneVersion, name, value, ttl)
+ if err != nil {
+ return err
+ }
+ err = d.setZoneVersion(newZoneID, newZoneVersion)
+ if err != nil {
+ return err
+ }
+ err = d.setZone(authZone, newZoneID)
+ if err != nil {
+ return err
+ }
+ // save data necessary for CleanUp
+ d.inProgressFQDNs[fqdn] = inProgressInfo{
+ zoneID: zoneID,
+ newZoneID: newZoneID,
+ authZone: authZone,
+ }
+ d.inProgressAuthZones[authZone] = struct{}{}
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified
+// parameters. It does this by restoring the old Gandi DNS zone and
+// removing the temporary one created by Present.
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+ // acquire lock and retrieve zoneID, newZoneID and authZone
+ d.inProgressMu.Lock()
+ defer d.inProgressMu.Unlock()
+ if _, ok := d.inProgressFQDNs[fqdn]; !ok {
+ // if there is no cleanup information then just return
+ return nil
+ }
+ zoneID := d.inProgressFQDNs[fqdn].zoneID
+ newZoneID := d.inProgressFQDNs[fqdn].newZoneID
+ authZone := d.inProgressFQDNs[fqdn].authZone
+ delete(d.inProgressFQDNs, fqdn)
+ delete(d.inProgressAuthZones, authZone)
+ // perform API actions to restore old gandi zone for authZone
+ err := d.setZone(authZone, zoneID)
+ if err != nil {
+ return err
+ }
+ err = d.deleteZone(newZoneID)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// Timeout returns the values (40*time.Minute, 60*time.Second) which
+// are used by the acme package as timeout and check interval values
+// when checking for DNS record propagation with Gandi.
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return 40 * time.Minute, 60 * time.Second
+}
+
+// types for XML-RPC method calls and parameters
+
+type param interface {
+ param()
+}
+type paramString struct {
+ XMLName xml.Name `xml:"param"`
+ Value string `xml:"value>string"`
+}
+type paramInt struct {
+ XMLName xml.Name `xml:"param"`
+ Value int `xml:"value>int"`
+}
+
+type structMember interface {
+ structMember()
+}
+type structMemberString struct {
+ Name string `xml:"name"`
+ Value string `xml:"value>string"`
+}
+type structMemberInt struct {
+ Name string `xml:"name"`
+ Value int `xml:"value>int"`
+}
+type paramStruct struct {
+ XMLName xml.Name `xml:"param"`
+ StructMembers []structMember `xml:"value>struct>member"`
+}
+
+func (p paramString) param() {}
+func (p paramInt) param() {}
+func (m structMemberString) structMember() {}
+func (m structMemberInt) structMember() {}
+func (p paramStruct) param() {}
+
+type methodCall struct {
+ XMLName xml.Name `xml:"methodCall"`
+ MethodName string `xml:"methodName"`
+ Params []param `xml:"params"`
+}
+
+// types for XML-RPC responses
+
+type response interface {
+ faultCode() int
+ faultString() string
+}
+
+type responseFault struct {
+ FaultCode int `xml:"fault>value>struct>member>value>int"`
+ FaultString string `xml:"fault>value>struct>member>value>string"`
+}
+
+func (r responseFault) faultCode() int { return r.FaultCode }
+func (r responseFault) faultString() string { return r.FaultString }
+
+type responseStruct struct {
+ responseFault
+ StructMembers []struct {
+ Name string `xml:"name"`
+ ValueInt int `xml:"value>int"`
+ } `xml:"params>param>value>struct>member"`
+}
+
+type responseInt struct {
+ responseFault
+ Value int `xml:"params>param>value>int"`
+}
+
+type responseBool struct {
+ responseFault
+ Value bool `xml:"params>param>value>boolean"`
+}
+
+// POSTing/Marshalling/Unmarshalling
+
+type rpcError struct {
+ faultCode int
+ faultString string
+}
+
+func (e rpcError) Error() string {
+ return fmt.Sprintf(
+ "Gandi DNS: RPC Error: (%d) %s", e.faultCode, e.faultString)
+}
+
+func httpPost(url string, bodyType string, body io.Reader) ([]byte, error) {
+ client := http.Client{Timeout: 60 * time.Second}
+ resp, err := client.Post(url, bodyType, body)
+ if err != nil {
+ return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
+ }
+ defer resp.Body.Close()
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
+ }
+ return b, nil
+}
+
+// rpcCall makes an XML-RPC call to Gandi's RPC endpoint by
+// marshalling the data given in the call argument to XML and sending
+// that via HTTP Post to Gandi. The response is then unmarshalled into
+// the resp argument.
+func rpcCall(call *methodCall, resp response) error {
+ // marshal
+ b, err := xml.MarshalIndent(call, "", " ")
+ if err != nil {
+ return fmt.Errorf("Gandi DNS: Marshal Error: %v", err)
+ }
+ // post
+ b = append([]byte(`<?xml version="1.0"?>`+"\n"), b...)
+ respBody, err := httpPost(endpoint, "text/xml", bytes.NewReader(b))
+ if err != nil {
+ return err
+ }
+ // unmarshal
+ err = xml.Unmarshal(respBody, resp)
+ if err != nil {
+ return fmt.Errorf("Gandi DNS: Unmarshal Error: %v", err)
+ }
+ if resp.faultCode() != 0 {
+ return rpcError{
+ faultCode: resp.faultCode(), faultString: resp.faultString()}
+ }
+ return nil
+}
+
+// functions to perform API actions
+
+func (d *DNSProvider) getZoneID(domain string) (int, error) {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.info",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramString{Value: domain},
+ },
+ }, resp)
+ if err != nil {
+ return 0, err
+ }
+ var zoneID int
+ for _, member := range resp.StructMembers {
+ if member.Name == "zone_id" {
+ zoneID = member.ValueInt
+ }
+ }
+ if zoneID == 0 {
+ return 0, fmt.Errorf(
+ "Gandi DNS: Could not determine zone_id for %s", domain)
+ }
+ return zoneID, nil
+}
+
+func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.clone",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ paramInt{Value: 0},
+ paramStruct{
+ StructMembers: []structMember{
+ structMemberString{
+ Name: "name",
+ Value: name,
+ }},
+ },
+ },
+ }, resp)
+ if err != nil {
+ return 0, err
+ }
+ var newZoneID int
+ for _, member := range resp.StructMembers {
+ if member.Name == "id" {
+ newZoneID = member.ValueInt
+ }
+ }
+ if newZoneID == 0 {
+ return 0, fmt.Errorf("Gandi DNS: Could not determine cloned zone_id")
+ }
+ return newZoneID, nil
+}
+
+func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) {
+ resp := &responseInt{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.version.new",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ },
+ }, resp)
+ if err != nil {
+ return 0, err
+ }
+ if resp.Value == 0 {
+ return 0, fmt.Errorf("Gandi DNS: Could not create new zone version")
+ }
+ return resp.Value, nil
+}
+
+func (d *DNSProvider) addTXTRecord(zoneID int, version int, name string, value string, ttl int) error {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.record.add",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ paramInt{Value: version},
+ paramStruct{
+ StructMembers: []structMember{
+ structMemberString{
+ Name: "type",
+ Value: "TXT",
+ }, structMemberString{
+ Name: "name",
+ Value: name,
+ }, structMemberString{
+ Name: "value",
+ Value: value,
+ }, structMemberInt{
+ Name: "ttl",
+ Value: ttl,
+ }},
+ },
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (d *DNSProvider) setZoneVersion(zoneID int, version int) error {
+ resp := &responseBool{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.version.set",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ paramInt{Value: version},
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ if !resp.Value {
+ return fmt.Errorf("Gandi DNS: could not set zone version")
+ }
+ return nil
+}
+
+func (d *DNSProvider) setZone(domain string, zoneID int) error {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.set",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramString{Value: domain},
+ paramInt{Value: zoneID},
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ var respZoneID int
+ for _, member := range resp.StructMembers {
+ if member.Name == "zone_id" {
+ respZoneID = member.ValueInt
+ }
+ }
+ if respZoneID != zoneID {
+ return fmt.Errorf(
+ "Gandi DNS: Could not set new zone_id for %s", domain)
+ }
+ return nil
+}
+
+func (d *DNSProvider) deleteZone(zoneID int) error {
+ resp := &responseBool{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.delete",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ if !resp.Value {
+ return fmt.Errorf("Gandi DNS: could not delete zone_id")
+ }
+ return nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi_test.go b/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi_test.go
new file mode 100644
index 000000000..15919e2eb
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi_test.go
@@ -0,0 +1,939 @@
+package gandi
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// stagingServer is the Let's Encrypt staging server used by the live test
+const stagingServer = "https://acme-staging.api.letsencrypt.org/directory"
+
+// user implements acme.User and is used by the live test
+type user struct {
+ Email string
+ Registration *acme.RegistrationResource
+ key crypto.PrivateKey
+}
+
+func (u *user) GetEmail() string {
+ return u.Email
+}
+func (u *user) GetRegistration() *acme.RegistrationResource {
+ return u.Registration
+}
+func (u *user) GetPrivateKey() crypto.PrivateKey {
+ return u.key
+}
+
+// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC
+// Server, whose responses are predetermined for particular requests.
+func TestDNSProvider(t *testing.T) {
+ fakeAPIKey := "123412341234123412341234"
+ fakeKeyAuth := "XXXX"
+ provider, err := NewDNSProviderCredentials(fakeAPIKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ regexpDate, err := regexp.Compile(`\[ACME Challenge [^\]:]*:[^\]]*\]`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // start fake RPC server
+ fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Header.Get("Content-Type") != "text/xml" {
+ t.Fatalf("Content-Type: text/xml header not found")
+ }
+ req, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ req = regexpDate.ReplaceAllLiteral(
+ req, []byte(`[ACME Challenge 01 Jan 16 00:00 +0000]`))
+ resp, ok := serverResponses[string(req)]
+ if !ok {
+ t.Fatalf("Server response for request not found")
+ }
+ _, err = io.Copy(w, strings.NewReader(resp))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer fakeServer.Close()
+ // define function to override findZoneByFqdn with
+ fakeFindZoneByFqdn := func(fqdn string, nameserver []string) (string, error) {
+ return "example.com.", nil
+ }
+ // override gandi endpoint and findZoneByFqdn function
+ savedEndpoint, savedFindZoneByFqdn := endpoint, findZoneByFqdn
+ defer func() {
+ endpoint, findZoneByFqdn = savedEndpoint, savedFindZoneByFqdn
+ }()
+ endpoint, findZoneByFqdn = fakeServer.URL+"/", fakeFindZoneByFqdn
+ // run Present
+ err = provider.Present("abc.def.example.com", "", fakeKeyAuth)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // run CleanUp
+ err = provider.CleanUp("abc.def.example.com", "", fakeKeyAuth)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+// TestDNSProviderLive performs a live test to obtain a certificate
+// using the Let's Encrypt staging server. It runs provided that both
+// the environment variables GANDI_API_KEY and GANDI_TEST_DOMAIN are
+// set. Otherwise the test is skipped.
+//
+// To complete this test, go test must be run with the -timeout=40m
+// flag, since the default timeout of 10m is insufficient.
+func TestDNSProviderLive(t *testing.T) {
+ apiKey := os.Getenv("GANDI_API_KEY")
+ domain := os.Getenv("GANDI_TEST_DOMAIN")
+ if apiKey == "" || domain == "" {
+ t.Skip("skipping live test")
+ }
+ // create a user.
+ const rsaKeySize = 2048
+ privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize)
+ if err != nil {
+ t.Fatal(err)
+ }
+ myUser := user{
+ Email: "test@example.com",
+ key: privateKey,
+ }
+ // create a client using staging server
+ client, err := acme.NewClient(stagingServer, &myUser, acme.RSA2048)
+ if err != nil {
+ t.Fatal(err)
+ }
+ provider, err := NewDNSProviderCredentials(apiKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = client.SetChallengeProvider(acme.DNS01, provider)
+ if err != nil {
+ t.Fatal(err)
+ }
+ client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
+ // register and agree tos
+ reg, err := client.Register()
+ if err != nil {
+ t.Fatal(err)
+ }
+ myUser.Registration = reg
+ err = client.AgreeToTOS()
+ if err != nil {
+ t.Fatal(err)
+ }
+ // complete the challenge
+ bundle := false
+ _, failures := client.ObtainCertificate([]string{domain}, bundle, nil)
+ if len(failures) > 0 {
+ t.Fatal(failures)
+ }
+}
+
+// serverResponses is the XML-RPC Request->Response map used by the
+// fake RPC server. It was generated by recording a real RPC session
+// which resulted in the successful issue of a cert, and then
+// anonymizing the RPC data.
+var serverResponses = map[string]string{
+ // Present Request->Response 1 (getZoneID)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.info</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <string>example.com.</string>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20160216T16:14:23</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_delete</name>
+<value><dateTime.iso8601>20170331T16:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>is_premium</name>
+<value><boolean>0</boolean></value>
+</member>
+<member>
+<name>date_hold_begin</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_registry_end</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>authinfo_expiration_date</name>
+<value><dateTime.iso8601>20161211T21:31:20</dateTime.iso8601></value>
+</member>
+<member>
+<name>contacts</name>
+<value><struct>
+<member>
+<name>owner</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>admin</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>bill</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>tech</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>reseller</name>
+<value><nil/></value></member>
+</struct></value>
+</member>
+<member>
+<name>nameservers</name>
+<value><array><data>
+<value><string>a.dns.gandi.net</string></value>
+<value><string>b.dns.gandi.net</string></value>
+<value><string>c.dns.gandi.net</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_restore_end</name>
+<value><dateTime.iso8601>20170501T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>id</name>
+<value><int>2222222</int></value>
+</member>
+<member>
+<name>authinfo</name>
+<value><string>ABCDABCDAB</string></value>
+</member>
+<member>
+<name>status</name>
+<value><array><data>
+<value><string>clientTransferProhibited</string></value>
+<value><string>serverTransferProhibited</string></value>
+</data></array></value>
+</member>
+<member>
+<name>tags</name>
+<value><array><data>
+</data></array></value>
+</member>
+<member>
+<name>date_hold_end</name>
+<value><dateTime.iso8601>20170401T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>services</name>
+<value><array><data>
+<value><string>gandidns</string></value>
+<value><string>gandimail</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_pending_delete_end</name>
+<value><dateTime.iso8601>20170506T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>zone_id</name>
+<value><int>1234567</int></value>
+</member>
+<member>
+<name>date_renew_begin</name>
+<value><dateTime.iso8601>20120101T00:00:00</dateTime.iso8601></value>
+</member>
+<member>
+<name>fqdn</name>
+<value><string>example.com</string></value>
+</member>
+<member>
+<name>autorenew</name>
+<value><nil/></value></member>
+<member>
+<name>date_registry_creation</name>
+<value><dateTime.iso8601>20150215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>tld</name>
+<value><string>org</string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20150215T03:04:06</dateTime.iso8601></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // Present Request->Response 2 (cloneZone)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.clone</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>1234567</int>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>0</int>
+ </value>
+ </param>
+ <param>
+ <value>
+ <struct>
+ <member>
+ <name>name</name>
+ <value>
+ <string>example.com [ACME Challenge 01 Jan 16 00:00 +0000]</string>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>name</name>
+<value><string>example.com [ACME Challenge 01 Jan 16 00:00 +0000]</string></value>
+</member>
+<member>
+<name>versions</name>
+<value><array><data>
+<value><int>1</int></value>
+</data></array></value>
+</member>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20160216T16:24:29</dateTime.iso8601></value>
+</member>
+<member>
+<name>id</name>
+<value><int>7654321</int></value>
+</member>
+<member>
+<name>owner</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>version</name>
+<value><int>1</int></value>
+</member>
+<member>
+<name>domains</name>
+<value><int>0</int></value>
+</member>
+<member>
+<name>public</name>
+<value><boolean>0</boolean></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // Present Request->Response 3 (newZoneVersion)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.version.new</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>7654321</int>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><int>2</int></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // Present Request->Response 4 (addTXTRecord)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.record.add</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>7654321</int>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>2</int>
+ </value>
+ </param>
+ <param>
+ <value>
+ <struct>
+ <member>
+ <name>type</name>
+ <value>
+ <string>TXT</string>
+ </value>
+ </member>
+ <member>
+ <name>name</name>
+ <value>
+ <string>_acme-challenge.abc.def</string>
+ </value>
+ </member>
+ <member>
+ <name>value</name>
+ <value>
+ <string>ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ</string>
+ </value>
+ </member>
+ <member>
+ <name>ttl</name>
+ <value>
+ <int>300</int>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>name</name>
+<value><string>_acme-challenge.abc.def</string></value>
+</member>
+<member>
+<name>type</name>
+<value><string>TXT</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>3333333333</int></value>
+</member>
+<member>
+<name>value</name>
+<value><string>"ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ"</string></value>
+</member>
+<member>
+<name>ttl</name>
+<value><int>300</int></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // Present Request->Response 5 (setZoneVersion)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.version.set</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>7654321</int>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>2</int>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><boolean>1</boolean></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // Present Request->Response 6 (setZone)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.set</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <string>example.com.</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>7654321</int>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20160216T16:14:23</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_delete</name>
+<value><dateTime.iso8601>20170331T16:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>is_premium</name>
+<value><boolean>0</boolean></value>
+</member>
+<member>
+<name>date_hold_begin</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_registry_end</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>authinfo_expiration_date</name>
+<value><dateTime.iso8601>20161211T21:31:20</dateTime.iso8601></value>
+</member>
+<member>
+<name>contacts</name>
+<value><struct>
+<member>
+<name>owner</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>admin</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>bill</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>tech</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>reseller</name>
+<value><nil/></value></member>
+</struct></value>
+</member>
+<member>
+<name>nameservers</name>
+<value><array><data>
+<value><string>a.dns.gandi.net</string></value>
+<value><string>b.dns.gandi.net</string></value>
+<value><string>c.dns.gandi.net</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_restore_end</name>
+<value><dateTime.iso8601>20170501T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>id</name>
+<value><int>2222222</int></value>
+</member>
+<member>
+<name>authinfo</name>
+<value><string>ABCDABCDAB</string></value>
+</member>
+<member>
+<name>status</name>
+<value><array><data>
+<value><string>clientTransferProhibited</string></value>
+<value><string>serverTransferProhibited</string></value>
+</data></array></value>
+</member>
+<member>
+<name>tags</name>
+<value><array><data>
+</data></array></value>
+</member>
+<member>
+<name>date_hold_end</name>
+<value><dateTime.iso8601>20170401T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>services</name>
+<value><array><data>
+<value><string>gandidns</string></value>
+<value><string>gandimail</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_pending_delete_end</name>
+<value><dateTime.iso8601>20170506T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>zone_id</name>
+<value><int>7654321</int></value>
+</member>
+<member>
+<name>date_renew_begin</name>
+<value><dateTime.iso8601>20120101T00:00:00</dateTime.iso8601></value>
+</member>
+<member>
+<name>fqdn</name>
+<value><string>example.com</string></value>
+</member>
+<member>
+<name>autorenew</name>
+<value><nil/></value></member>
+<member>
+<name>date_registry_creation</name>
+<value><dateTime.iso8601>20150215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>tld</name>
+<value><string>org</string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20150215T03:04:06</dateTime.iso8601></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // CleanUp Request->Response 1 (setZone)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.set</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <string>example.com.</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>1234567</int>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>date_updated</name>
+<value><dateTime.iso8601>20160216T16:24:38</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_delete</name>
+<value><dateTime.iso8601>20170331T16:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>is_premium</name>
+<value><boolean>0</boolean></value>
+</member>
+<member>
+<name>date_hold_begin</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>date_registry_end</name>
+<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>authinfo_expiration_date</name>
+<value><dateTime.iso8601>20161211T21:31:20</dateTime.iso8601></value>
+</member>
+<member>
+<name>contacts</name>
+<value><struct>
+<member>
+<name>owner</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>admin</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>bill</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>tech</name>
+<value><struct>
+<member>
+<name>handle</name>
+<value><string>LEGO-GANDI</string></value>
+</member>
+<member>
+<name>id</name>
+<value><int>111111</int></value>
+</member>
+</struct></value>
+</member>
+<member>
+<name>reseller</name>
+<value><nil/></value></member>
+</struct></value>
+</member>
+<member>
+<name>nameservers</name>
+<value><array><data>
+<value><string>a.dns.gandi.net</string></value>
+<value><string>b.dns.gandi.net</string></value>
+<value><string>c.dns.gandi.net</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_restore_end</name>
+<value><dateTime.iso8601>20170501T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>id</name>
+<value><int>2222222</int></value>
+</member>
+<member>
+<name>authinfo</name>
+<value><string>ABCDABCDAB</string></value>
+</member>
+<member>
+<name>status</name>
+<value><array><data>
+<value><string>clientTransferProhibited</string></value>
+<value><string>serverTransferProhibited</string></value>
+</data></array></value>
+</member>
+<member>
+<name>tags</name>
+<value><array><data>
+</data></array></value>
+</member>
+<member>
+<name>date_hold_end</name>
+<value><dateTime.iso8601>20170401T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>services</name>
+<value><array><data>
+<value><string>gandidns</string></value>
+<value><string>gandimail</string></value>
+</data></array></value>
+</member>
+<member>
+<name>date_pending_delete_end</name>
+<value><dateTime.iso8601>20170506T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>zone_id</name>
+<value><int>1234567</int></value>
+</member>
+<member>
+<name>date_renew_begin</name>
+<value><dateTime.iso8601>20120101T00:00:00</dateTime.iso8601></value>
+</member>
+<member>
+<name>fqdn</name>
+<value><string>example.com</string></value>
+</member>
+<member>
+<name>autorenew</name>
+<value><nil/></value></member>
+<member>
+<name>date_registry_creation</name>
+<value><dateTime.iso8601>20150215T02:04:06</dateTime.iso8601></value>
+</member>
+<member>
+<name>tld</name>
+<value><string>org</string></value>
+</member>
+<member>
+<name>date_created</name>
+<value><dateTime.iso8601>20150215T03:04:06</dateTime.iso8601></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+`,
+ // CleanUp Request->Response 2 (deleteZone)
+ `<?xml version="1.0"?>
+<methodCall>
+ <methodName>domain.zone.delete</methodName>
+ <param>
+ <value>
+ <string>123412341234123412341234</string>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>7654321</int>
+ </value>
+ </param>
+</methodCall>`: `<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><boolean>1</boolean></value>
+</param>
+</params>
+</methodResponse>
+`,
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go b/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go
new file mode 100644
index 000000000..b8d9951c9
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go
@@ -0,0 +1,158 @@
+// Package googlecloud implements a DNS provider for solving the DNS-01
+// challenge using Google Cloud DNS.
+package googlecloud
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+
+ "golang.org/x/net/context"
+ "golang.org/x/oauth2/google"
+
+ "google.golang.org/api/dns/v1"
+)
+
+// DNSProvider is an implementation of the DNSProvider interface.
+type DNSProvider struct {
+ project string
+ client *dns.Service
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Google Cloud
+// DNS. Credentials must be passed in the environment variable: GCE_PROJECT.
+func NewDNSProvider() (*DNSProvider, error) {
+ project := os.Getenv("GCE_PROJECT")
+ return NewDNSProviderCredentials(project)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for Google Cloud DNS.
+func NewDNSProviderCredentials(project string) (*DNSProvider, error) {
+ if project == "" {
+ return nil, fmt.Errorf("Google Cloud project name missing")
+ }
+
+ client, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to get Google Cloud client: %v", err)
+ }
+ svc, err := dns.New(client)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to create Google Cloud DNS service: %v", err)
+ }
+ return &DNSProvider{
+ project: project,
+ client: svc,
+ }, 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
+ }
+
+ rec := &dns.ResourceRecordSet{
+ Name: fqdn,
+ Rrdatas: []string{value},
+ Ttl: int64(ttl),
+ Type: "TXT",
+ }
+ change := &dns.Change{
+ Additions: []*dns.ResourceRecordSet{rec},
+ }
+
+ chg, err := c.client.Changes.Create(c.project, zone, change).Do()
+ if err != nil {
+ return err
+ }
+
+ // wait for change to be acknowledged
+ for chg.Status == "pending" {
+ time.Sleep(time.Second)
+
+ chg, err = c.client.Changes.Get(c.project, zone, chg.Id).Do()
+ if err != nil {
+ 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
+ }
+
+ records, err := c.findTxtRecords(zone, fqdn)
+ if err != nil {
+ return err
+ }
+
+ for _, rec := range records {
+ change := &dns.Change{
+ Deletions: []*dns.ResourceRecordSet{rec},
+ }
+ _, err = c.client.Changes.Create(c.project, zone, change).Do()
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Timeout customizes the timeout values used by the ACME package for checking
+// DNS record validity.
+func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return 180 * time.Second, 5 * time.Second
+}
+
+// getHostedZone returns the managed-zone
+func (c *DNSProvider) getHostedZone(domain string) (string, error) {
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return "", err
+ }
+
+ zones, err := c.client.ManagedZones.
+ List(c.project).
+ DnsName(authZone).
+ Do()
+ if err != nil {
+ return "", fmt.Errorf("GoogleCloud API call failed: %v", err)
+ }
+
+ if len(zones.ManagedZones) == 0 {
+ return "", fmt.Errorf("No matching GoogleCloud domain found for domain %s", authZone)
+ }
+
+ return zones.ManagedZones[0].Name, nil
+}
+
+func (c *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSet, error) {
+
+ recs, err := c.client.ResourceRecordSets.List(c.project, zone).Do()
+ if err != nil {
+ return nil, err
+ }
+
+ found := []*dns.ResourceRecordSet{}
+ for _, r := range recs.Rrsets {
+ if r.Type == "TXT" && r.Name == fqdn {
+ found = append(found, r)
+ }
+ }
+
+ return found, nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud_test.go b/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud_test.go
new file mode 100644
index 000000000..d73788163
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud_test.go
@@ -0,0 +1,85 @@
+package googlecloud
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "golang.org/x/net/context"
+ "golang.org/x/oauth2/google"
+ "google.golang.org/api/dns/v1"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ gcloudLiveTest bool
+ gcloudProject string
+ gcloudDomain string
+)
+
+func init() {
+ gcloudProject = os.Getenv("GCE_PROJECT")
+ gcloudDomain = os.Getenv("GCE_DOMAIN")
+ _, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)
+ if err == nil && len(gcloudProject) > 0 && len(gcloudDomain) > 0 {
+ gcloudLiveTest = true
+ }
+}
+
+func restoreGCloudEnv() {
+ os.Setenv("GCE_PROJECT", gcloudProject)
+}
+
+func TestNewDNSProviderValid(t *testing.T) {
+ if !gcloudLiveTest {
+ t.Skip("skipping live test (requires credentials)")
+ }
+ os.Setenv("GCE_PROJECT", "")
+ _, err := NewDNSProviderCredentials("my-project")
+ assert.NoError(t, err)
+ restoreGCloudEnv()
+}
+
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ if !gcloudLiveTest {
+ t.Skip("skipping live test (requires credentials)")
+ }
+ os.Setenv("GCE_PROJECT", "my-project")
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+ restoreGCloudEnv()
+}
+
+func TestNewDNSProviderMissingCredErr(t *testing.T) {
+ os.Setenv("GCE_PROJECT", "")
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "Google Cloud project name missing")
+ restoreGCloudEnv()
+}
+
+func TestLiveGoogleCloudPresent(t *testing.T) {
+ if !gcloudLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProviderCredentials(gcloudProject)
+ assert.NoError(t, err)
+
+ err = provider.Present(gcloudDomain, "", "123d==")
+ assert.NoError(t, err)
+}
+
+func TestLiveGoogleCloudCleanUp(t *testing.T) {
+ if !gcloudLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ time.Sleep(time.Second * 1)
+
+ provider, err := NewDNSProviderCredentials(gcloudProject)
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(gcloudDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/linode/linode.go b/vendor/github.com/xenolf/lego/providers/dns/linode/linode.go
new file mode 100644
index 000000000..a91d2b489
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/linode/linode.go
@@ -0,0 +1,131 @@
+// Package linode implements a DNS provider for solving the DNS-01 challenge
+// using Linode DNS.
+package linode
+
+import (
+ "errors"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/timewasted/linode/dns"
+ "github.com/xenolf/lego/acme"
+)
+
+const (
+ dnsMinTTLSecs = 300
+ dnsUpdateFreqMins = 15
+ dnsUpdateFudgeSecs = 120
+)
+
+type hostedZoneInfo struct {
+ domainId int
+ resourceName string
+}
+
+// DNSProvider implements the acme.ChallengeProvider interface.
+type DNSProvider struct {
+ linode *dns.DNS
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Linode.
+// Credentials must be passed in the environment variable: LINODE_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ apiKey := os.Getenv("LINODE_API_KEY")
+ return NewDNSProviderCredentials(apiKey)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for Linode.
+func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
+ if len(apiKey) == 0 {
+ return nil, errors.New("Linode credentials missing")
+ }
+
+ return &DNSProvider{
+ linode: dns.New(apiKey),
+ }, nil
+}
+
+// Timeout returns the timeout and interval to use when checking for DNS
+// propagation. Adjusting here to cope with spikes in propagation times.
+func (p *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ // Since Linode only updates their zone files every X minutes, we need
+ // to figure out how many minutes we have to wait until we hit the next
+ // interval of X. We then wait another couple of minutes, just to be
+ // safe. Hopefully at some point during all of this, the record will
+ // have propagated throughout Linode's network.
+ minsRemaining := dnsUpdateFreqMins - (time.Now().Minute() % dnsUpdateFreqMins)
+
+ timeout = (time.Duration(minsRemaining) * time.Minute) +
+ (dnsMinTTLSecs * time.Second) +
+ (dnsUpdateFudgeSecs * time.Second)
+ interval = 15 * time.Second
+ return
+}
+
+// Present creates a TXT record using the specified parameters.
+func (p *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
+ zone, err := p.getHostedZoneInfo(fqdn)
+ if err != nil {
+ return err
+ }
+
+ if _, err = p.linode.CreateDomainResourceTXT(zone.domainId, acme.UnFqdn(fqdn), value, 60); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
+ zone, err := p.getHostedZoneInfo(fqdn)
+ if err != nil {
+ return err
+ }
+
+ // Get all TXT records for the specified domain.
+ resources, err := p.linode.GetResourcesByType(zone.domainId, "TXT")
+ if err != nil {
+ return err
+ }
+
+ // Remove the specified resource, if it exists.
+ for _, resource := range resources {
+ if resource.Name == zone.resourceName && resource.Target == value {
+ resp, err := p.linode.DeleteDomainResource(resource.DomainID, resource.ResourceID)
+ if err != nil {
+ return err
+ }
+ if resp.ResourceID != resource.ResourceID {
+ return errors.New("Error deleting resource: resource IDs do not match!")
+ }
+ break
+ }
+ }
+
+ return nil
+}
+
+func (p *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) {
+ // Lookup the zone that handles the specified FQDN.
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return nil, err
+ }
+ resourceName := strings.TrimSuffix(fqdn, "."+authZone)
+
+ // Query the authority zone.
+ domain, err := p.linode.GetDomain(acme.UnFqdn(authZone))
+ if err != nil {
+ return nil, err
+ }
+
+ return &hostedZoneInfo{
+ domainId: domain.DomainID,
+ resourceName: resourceName,
+ }, nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/linode/linode_test.go b/vendor/github.com/xenolf/lego/providers/dns/linode/linode_test.go
new file mode 100644
index 000000000..d9713a275
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/linode/linode_test.go
@@ -0,0 +1,317 @@
+package linode
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/timewasted/linode"
+ "github.com/timewasted/linode/dns"
+)
+
+type (
+ LinodeResponse struct {
+ Action string `json:"ACTION"`
+ Data interface{} `json:"DATA"`
+ Errors []linode.ResponseError `json:"ERRORARRAY"`
+ }
+ MockResponse struct {
+ Response interface{}
+ Errors []linode.ResponseError
+ }
+ MockResponseMap map[string]MockResponse
+)
+
+var (
+ apiKey string
+ isTestLive bool
+)
+
+func init() {
+ apiKey = os.Getenv("LINODE_API_KEY")
+ isTestLive = len(apiKey) != 0
+}
+
+func restoreEnv() {
+ os.Setenv("LINODE_API_KEY", apiKey)
+}
+
+func newMockServer(t *testing.T, responses MockResponseMap) *httptest.Server {
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Ensure that we support the requested action.
+ action := r.URL.Query().Get("api_action")
+ resp, ok := responses[action]
+ if !ok {
+ msg := fmt.Sprintf("Unsupported mock action: %s", action)
+ require.FailNow(t, msg)
+ }
+
+ // Build the response that the server will return.
+ linodeResponse := LinodeResponse{
+ Action: action,
+ Data: resp.Response,
+ Errors: resp.Errors,
+ }
+ rawResponse, err := json.Marshal(linodeResponse)
+ if err != nil {
+ msg := fmt.Sprintf("Failed to JSON encode response: %v", err)
+ require.FailNow(t, msg)
+ }
+
+ // Send the response.
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ w.Write(rawResponse)
+ }))
+
+ time.Sleep(100 * time.Millisecond)
+ return srv
+}
+
+func TestNewDNSProviderWithEnv(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+}
+
+func TestNewDNSProviderWithoutEnv(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "Linode credentials missing")
+}
+
+func TestNewDNSProviderCredentialsWithKey(t *testing.T) {
+ _, err := NewDNSProviderCredentials("testing")
+ assert.NoError(t, err)
+}
+
+func TestNewDNSProviderCredentialsWithoutKey(t *testing.T) {
+ _, err := NewDNSProviderCredentials("")
+ assert.EqualError(t, err, "Linode credentials missing")
+}
+
+func TestDNSProvider_Present(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: domain,
+ DomainID: 1234,
+ },
+ },
+ },
+ "domain.resource.create": MockResponse{
+ Response: dns.ResourceResponse{
+ ResourceID: 1234,
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.Present(domain, "", keyAuth)
+ assert.NoError(t, err)
+}
+
+func TestDNSProvider_PresentNoDomain(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: "foobar.com",
+ DomainID: 1234,
+ },
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.Present(domain, "", keyAuth)
+ assert.EqualError(t, err, "dns: requested domain not found")
+}
+
+func TestDNSProvider_PresentCreateFailed(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: domain,
+ DomainID: 1234,
+ },
+ },
+ },
+ "domain.resource.create": MockResponse{
+ Response: nil,
+ Errors: []linode.ResponseError{
+ linode.ResponseError{
+ Code: 1234,
+ Message: "Failed to create domain resource",
+ },
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.Present(domain, "", keyAuth)
+ assert.EqualError(t, err, "Failed to create domain resource")
+}
+
+func TestDNSProvider_PresentLive(t *testing.T) {
+ if !isTestLive {
+ t.Skip("Skipping live test")
+ }
+}
+
+func TestDNSProvider_CleanUp(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: domain,
+ DomainID: 1234,
+ },
+ },
+ },
+ "domain.resource.list": MockResponse{
+ Response: []dns.Resource{
+ dns.Resource{
+ DomainID: 1234,
+ Name: "_acme-challenge",
+ ResourceID: 1234,
+ Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM",
+ Type: "TXT",
+ },
+ },
+ },
+ "domain.resource.delete": MockResponse{
+ Response: dns.ResourceResponse{
+ ResourceID: 1234,
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.CleanUp(domain, "", keyAuth)
+ assert.NoError(t, err)
+}
+
+func TestDNSProvider_CleanUpNoDomain(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: "foobar.com",
+ DomainID: 1234,
+ },
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.CleanUp(domain, "", keyAuth)
+ assert.EqualError(t, err, "dns: requested domain not found")
+}
+
+func TestDNSProvider_CleanUpDeleteFailed(t *testing.T) {
+ os.Setenv("LINODE_API_KEY", "testing")
+ defer restoreEnv()
+ p, err := NewDNSProvider()
+ assert.NoError(t, err)
+
+ domain := "example.com"
+ keyAuth := "dGVzdGluZw=="
+ mockResponses := MockResponseMap{
+ "domain.list": MockResponse{
+ Response: []dns.Domain{
+ dns.Domain{
+ Domain: domain,
+ DomainID: 1234,
+ },
+ },
+ },
+ "domain.resource.list": MockResponse{
+ Response: []dns.Resource{
+ dns.Resource{
+ DomainID: 1234,
+ Name: "_acme-challenge",
+ ResourceID: 1234,
+ Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM",
+ Type: "TXT",
+ },
+ },
+ },
+ "domain.resource.delete": MockResponse{
+ Response: nil,
+ Errors: []linode.ResponseError{
+ linode.ResponseError{
+ Code: 1234,
+ Message: "Failed to delete domain resource",
+ },
+ },
+ },
+ }
+ mockSrv := newMockServer(t, mockResponses)
+ defer mockSrv.Close()
+ p.linode.ToLinode().SetEndpoint(mockSrv.URL)
+
+ err = p.CleanUp(domain, "", keyAuth)
+ assert.EqualError(t, err, "Failed to delete domain resource")
+}
+
+func TestDNSProvider_CleanUpLive(t *testing.T) {
+ if !isTestLive {
+ t.Skip("Skipping live test")
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap.go b/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap.go
new file mode 100644
index 000000000..d7eb40935
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap.go
@@ -0,0 +1,416 @@
+// 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"
+ httpClient = http.Client{Timeout: 60 * time.Second}
+)
+
+// 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 DNSProvider instance configured for namecheap.
+// Credentials must be passed in the environment variables: NAMECHEAP_API_USER
+// and NAMECHEAP_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ apiUser := os.Getenv("NAMECHEAP_API_USER")
+ apiKey := os.Getenv("NAMECHEAP_API_KEY")
+ return NewDNSProviderCredentials(apiUser, apiKey)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for namecheap.
+func NewDNSProviderCredentials(apiUser, apiKey string) (*DNSProvider, error) {
+ 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
+}
+
+// Timeout returns the timeout and interval to use when checking for DNS
+// propagation. 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 := httpClient.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 := httpClient.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, &gtr); 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 := httpClient.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 := httpClient.PostForm(d.baseURL, values)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 400 {
+ return fmt.Errorf("setHosts HTTP error %d", resp.StatusCode)
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ type SetHostsResponse struct {
+ XMLName xml.Name `xml:"ApiResponse"`
+ Status string `xml:"Status,attr"`
+ Errors []apierror `xml:"Errors>Error"`
+ Result struct {
+ IsSuccess string `xml:",attr"`
+ } `xml:"CommandResponse>DomainDNSSetHostsResult"`
+ }
+
+ var shr SetHostsResponse
+ if err := xml.Unmarshal(body, &shr); err != nil {
+ return err
+ }
+ if len(shr.Errors) > 0 {
+ return fmt.Errorf("Namecheap error: %s [%d]",
+ shr.Errors[0].Description, shr.Errors[0].Number)
+ }
+ if shr.Result.IsSuccess != "true" {
+ return fmt.Errorf("Namecheap setHosts failed.")
+ }
+
+ return nil
+}
+
+// addChallengeRecord adds a DNS challenge TXT record to a list of namecheap
+// host records.
+func (d *DNSProvider) addChallengeRecord(ch *challenge, hosts *[]host) {
+ host := host{
+ Name: ch.key,
+ Type: "TXT",
+ Address: ch.keyValue,
+ MXPref: "10",
+ TTL: "120",
+ }
+
+ // If there's already a TXT record with the same name, replace it.
+ for i, h := range *hosts {
+ if h.Name == ch.key && h.Type == "TXT" {
+ (*hosts)[i] = host
+ return
+ }
+ }
+
+ // No record was replaced, so add a new one.
+ *hosts = append(*hosts, host)
+}
+
+// removeChallengeRecord removes a DNS challenge TXT record from a list of
+// namecheap host records. Return true if a record was removed.
+func (d *DNSProvider) removeChallengeRecord(ch *challenge, hosts *[]host) bool {
+ // Find the challenge TXT record and remove it if found.
+ for i, h := range *hosts {
+ if h.Name == ch.key && h.Type == "TXT" {
+ *hosts = append((*hosts)[:i], (*hosts)[i+1:]...)
+ return true
+ }
+ }
+
+ return false
+}
+
+// Present installs a TXT record for the DNS challenge.
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ tlds, err := d.getTLDs()
+ if err != nil {
+ return err
+ }
+
+ ch, err := newChallenge(domain, keyAuth, tlds)
+ if err != nil {
+ return err
+ }
+
+ hosts, err := d.getHosts(ch)
+ if err != nil {
+ return err
+ }
+
+ d.addChallengeRecord(ch, &hosts)
+
+ if debug {
+ for _, h := range hosts {
+ fmt.Printf(
+ "%-5.5s %-30.30s %-6s %-70.70s\n",
+ h.Type, h.Name, h.TTL, h.Address)
+ }
+ }
+
+ return d.setHosts(ch, hosts)
+}
+
+// CleanUp removes a TXT record used for a previous DNS challenge.
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ tlds, err := d.getTLDs()
+ if err != nil {
+ return err
+ }
+
+ ch, err := newChallenge(domain, keyAuth, tlds)
+ if err != nil {
+ return err
+ }
+
+ hosts, err := d.getHosts(ch)
+ if err != nil {
+ return err
+ }
+
+ if removed := d.removeChallengeRecord(ch, &hosts); !removed {
+ return nil
+ }
+
+ return d.setHosts(ch, hosts)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap_test.go b/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap_test.go
new file mode 100644
index 000000000..0631d4a3e
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/namecheap/namecheap_test.go
@@ -0,0 +1,402 @@
+package namecheap
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+)
+
+var (
+ fakeUser = "foo"
+ fakeKey = "bar"
+ fakeClientIP = "10.0.0.1"
+
+ tlds = map[string]string{
+ "com.au": "com.au",
+ "com": "com",
+ "co.uk": "co.uk",
+ "uk": "uk",
+ "edu": "edu",
+ "co.com": "co.com",
+ "za.com": "za.com",
+ }
+)
+
+func assertEq(t *testing.T, variable, got, want string) {
+ if got != want {
+ t.Errorf("Expected %s to be '%s' but got '%s'", variable, want, got)
+ }
+}
+
+func assertHdr(tc *testcase, t *testing.T, values *url.Values) {
+ ch, _ := newChallenge(tc.domain, "", tlds)
+
+ assertEq(t, "ApiUser", values.Get("ApiUser"), fakeUser)
+ assertEq(t, "ApiKey", values.Get("ApiKey"), fakeKey)
+ assertEq(t, "UserName", values.Get("UserName"), fakeUser)
+ assertEq(t, "ClientIp", values.Get("ClientIp"), fakeClientIP)
+ assertEq(t, "SLD", values.Get("SLD"), ch.sld)
+ assertEq(t, "TLD", values.Get("TLD"), ch.tld)
+}
+
+func mockServer(tc *testcase, t *testing.T, w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+
+ case "GET":
+ values := r.URL.Query()
+ cmd := values.Get("Command")
+ switch cmd {
+ case "namecheap.domains.dns.getHosts":
+ assertHdr(tc, t, &values)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, tc.getHostsResponse)
+ case "namecheap.domains.getTldList":
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, responseGetTlds)
+ default:
+ t.Errorf("Unexpected GET command: %s", cmd)
+ }
+
+ case "POST":
+ r.ParseForm()
+ values := r.Form
+ cmd := values.Get("Command")
+ switch cmd {
+ case "namecheap.domains.dns.setHosts":
+ assertHdr(tc, t, &values)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, tc.setHostsResponse)
+ default:
+ t.Errorf("Unexpected POST command: %s", cmd)
+ }
+
+ default:
+ t.Errorf("Unexpected http method: %s", r.Method)
+
+ }
+}
+
+func testGetHosts(tc *testcase, t *testing.T) {
+ mock := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ mockServer(tc, t, w, r)
+ }))
+ defer mock.Close()
+
+ prov := &DNSProvider{
+ baseURL: mock.URL,
+ apiUser: fakeUser,
+ apiKey: fakeKey,
+ clientIP: fakeClientIP,
+ }
+
+ ch, _ := newChallenge(tc.domain, "", tlds)
+ hosts, err := prov.getHosts(ch)
+ if tc.errString != "" {
+ if err == nil || err.Error() != tc.errString {
+ t.Errorf("Namecheap getHosts case %s expected error", tc.name)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Namecheap getHosts case %s failed\n%v", tc.name, err)
+ }
+ }
+
+next1:
+ for _, h := range hosts {
+ for _, th := range tc.hosts {
+ if h == th {
+ continue next1
+ }
+ }
+ t.Errorf("getHosts case %s unexpected record [%s:%s:%s]",
+ tc.name, h.Type, h.Name, h.Address)
+ }
+
+next2:
+ for _, th := range tc.hosts {
+ for _, h := range hosts {
+ if h == th {
+ continue next2
+ }
+ }
+ t.Errorf("getHosts case %s missing record [%s:%s:%s]",
+ tc.name, th.Type, th.Name, th.Address)
+ }
+}
+
+func mockDNSProvider(url string) *DNSProvider {
+ return &DNSProvider{
+ baseURL: url,
+ apiUser: fakeUser,
+ apiKey: fakeKey,
+ clientIP: fakeClientIP,
+ }
+}
+
+func testSetHosts(tc *testcase, t *testing.T) {
+ mock := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ mockServer(tc, t, w, r)
+ }))
+ defer mock.Close()
+
+ prov := mockDNSProvider(mock.URL)
+ ch, _ := newChallenge(tc.domain, "", tlds)
+ hosts, err := prov.getHosts(ch)
+ if tc.errString != "" {
+ if err == nil || err.Error() != tc.errString {
+ t.Errorf("Namecheap getHosts case %s expected error", tc.name)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Namecheap getHosts case %s failed\n%v", tc.name, err)
+ }
+ }
+ if err != nil {
+ return
+ }
+
+ err = prov.setHosts(ch, hosts)
+ if err != nil {
+ t.Errorf("Namecheap setHosts case %s failed", tc.name)
+ }
+}
+
+func testPresent(tc *testcase, t *testing.T) {
+ mock := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ mockServer(tc, t, w, r)
+ }))
+ defer mock.Close()
+
+ prov := mockDNSProvider(mock.URL)
+ err := prov.Present(tc.domain, "", "dummyKey")
+ if tc.errString != "" {
+ if err == nil || err.Error() != tc.errString {
+ t.Errorf("Namecheap Present case %s expected error", tc.name)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Namecheap Present case %s failed\n%v", tc.name, err)
+ }
+ }
+}
+
+func testCleanUp(tc *testcase, t *testing.T) {
+ mock := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ mockServer(tc, t, w, r)
+ }))
+ defer mock.Close()
+
+ prov := mockDNSProvider(mock.URL)
+ err := prov.CleanUp(tc.domain, "", "dummyKey")
+ if tc.errString != "" {
+ if err == nil || err.Error() != tc.errString {
+ t.Errorf("Namecheap CleanUp case %s expected error", tc.name)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Namecheap CleanUp case %s failed\n%v", tc.name, err)
+ }
+ }
+}
+
+func TestNamecheap(t *testing.T) {
+ for _, tc := range testcases {
+ testGetHosts(&tc, t)
+ testSetHosts(&tc, t)
+ testPresent(&tc, t)
+ testCleanUp(&tc, t)
+ }
+}
+
+func TestNamecheapDomainSplit(t *testing.T) {
+ tests := []struct {
+ domain string
+ valid bool
+ tld string
+ sld string
+ host string
+ }{
+ {"a.b.c.test.co.uk", true, "co.uk", "test", "a.b.c"},
+ {"test.co.uk", true, "co.uk", "test", ""},
+ {"test.com", true, "com", "test", ""},
+ {"test.co.com", true, "co.com", "test", ""},
+ {"www.test.com.au", true, "com.au", "test", "www"},
+ {"www.za.com", true, "za.com", "www", ""},
+ {"", false, "", "", ""},
+ {"a", false, "", "", ""},
+ {"com", false, "", "", ""},
+ {"co.com", false, "", "", ""},
+ {"co.uk", false, "", "", ""},
+ {"test.au", false, "", "", ""},
+ {"za.com", false, "", "", ""},
+ {"www.za", false, "", "", ""},
+ {"www.test.au", false, "", "", ""},
+ {"www.test.unk", false, "", "", ""},
+ }
+
+ for _, test := range tests {
+ valid := true
+ ch, err := newChallenge(test.domain, "", tlds)
+ if err != nil {
+ valid = false
+ }
+
+ if test.valid && !valid {
+ t.Errorf("Expected '%s' to split", test.domain)
+ } else if !test.valid && valid {
+ t.Errorf("Expected '%s' to produce error", test.domain)
+ }
+
+ if test.valid && valid {
+ assertEq(t, "domain", ch.domain, test.domain)
+ assertEq(t, "tld", ch.tld, test.tld)
+ assertEq(t, "sld", ch.sld, test.sld)
+ assertEq(t, "host", ch.host, test.host)
+ }
+ }
+}
+
+type testcase struct {
+ name string
+ domain string
+ hosts []host
+ errString string
+ getHostsResponse string
+ setHostsResponse string
+}
+
+var testcases = []testcase{
+ {
+ "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>`
diff --git a/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh.go b/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh.go
new file mode 100644
index 000000000..290a8d7df
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh.go
@@ -0,0 +1,159 @@
+// Package OVH implements a DNS provider for solving the DNS-01
+// challenge using OVH DNS.
+package ovh
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "sync"
+
+ "github.com/ovh/go-ovh/ovh"
+ "github.com/xenolf/lego/acme"
+)
+
+// OVH API reference: https://eu.api.ovh.com/
+// Create a Token: https://eu.api.ovh.com/createToken/
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface
+// that uses OVH's REST API to manage TXT records for a domain.
+type DNSProvider struct {
+ client *ovh.Client
+ recordIDs map[string]int
+ recordIDsMu sync.Mutex
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for OVH
+// Credentials must be passed in the environment variable:
+// OVH_ENDPOINT : it must be ovh-eu or ovh-ca
+// OVH_APPLICATION_KEY
+// OVH_APPLICATION_SECRET
+// OVH_CONSUMER_KEY
+func NewDNSProvider() (*DNSProvider, error) {
+ apiEndpoint := os.Getenv("OVH_ENDPOINT")
+ applicationKey := os.Getenv("OVH_APPLICATION_KEY")
+ applicationSecret := os.Getenv("OVH_APPLICATION_SECRET")
+ consumerKey := os.Getenv("OVH_CONSUMER_KEY")
+ return NewDNSProviderCredentials(apiEndpoint, applicationKey, applicationSecret, consumerKey)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for OVH.
+func NewDNSProviderCredentials(apiEndpoint, applicationKey, applicationSecret, consumerKey string) (*DNSProvider, error) {
+ if apiEndpoint == "" || applicationKey == "" || applicationSecret == "" || consumerKey == "" {
+ return nil, fmt.Errorf("OVH credentials missing")
+ }
+
+ ovhClient, _ := ovh.NewClient(
+ apiEndpoint,
+ applicationKey,
+ applicationSecret,
+ consumerKey,
+ )
+
+ return &DNSProvider{
+ client: ovhClient,
+ recordIDs: make(map[string]int),
+ }, nil
+}
+
+// Present creates a TXT record to fulfil the dns-01 challenge.
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+
+ // txtRecordRequest represents the request body to DO's API to make a TXT record
+ type txtRecordRequest struct {
+ FieldType string `json:"fieldType"`
+ SubDomain string `json:"subDomain"`
+ Target string `json:"target"`
+ TTL int `json:"ttl"`
+ }
+
+ // txtRecordResponse represents a response from DO's API after making a TXT record
+ type txtRecordResponse struct {
+ ID int `json:"id"`
+ FieldType string `json:"fieldType"`
+ SubDomain string `json:"subDomain"`
+ Target string `json:"target"`
+ TTL int `json:"ttl"`
+ Zone string `json:"zone"`
+ }
+
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+
+ // Parse domain name
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
+ }
+
+ authZone = acme.UnFqdn(authZone)
+ subDomain := d.extractRecordName(fqdn, authZone)
+
+ reqURL := fmt.Sprintf("/domain/zone/%s/record", authZone)
+ reqData := txtRecordRequest{FieldType: "TXT", SubDomain: subDomain, Target: value, TTL: ttl}
+ var respData txtRecordResponse
+
+ // Create TXT record
+ err = d.client.Post(reqURL, reqData, &respData)
+ if err != nil {
+ fmt.Printf("Error when call OVH api to add record : %q \n", err)
+ return err
+ }
+
+ // Apply the change
+ reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone)
+ err = d.client.Post(reqURL, nil, nil)
+ if err != nil {
+ fmt.Printf("Error when call OVH api to refresh zone : %q \n", err)
+ return err
+ }
+
+ d.recordIDsMu.Lock()
+ d.recordIDs[fqdn] = respData.ID
+ d.recordIDsMu.Unlock()
+
+ 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)
+
+ // get the record's unique ID from when we created it
+ d.recordIDsMu.Lock()
+ recordID, ok := d.recordIDs[fqdn]
+ d.recordIDsMu.Unlock()
+ if !ok {
+ return fmt.Errorf("unknown record ID for '%s'", fqdn)
+ }
+
+ authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
+ if err != nil {
+ return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
+ }
+
+ authZone = acme.UnFqdn(authZone)
+
+ reqURL := fmt.Sprintf("/domain/zone/%s/record/%d", authZone, recordID)
+
+ err = d.client.Delete(reqURL, nil)
+ if err != nil {
+ fmt.Printf("Error when call OVH api to delete challenge record : %q \n", err)
+ return err
+ }
+
+ // Delete record ID from map
+ d.recordIDsMu.Lock()
+ delete(d.recordIDs, fqdn)
+ d.recordIDsMu.Unlock()
+
+ return nil
+}
+
+func (d *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/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh_test.go b/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh_test.go
new file mode 100644
index 000000000..47da60e57
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/ovh/ovh_test.go
@@ -0,0 +1,103 @@
+package ovh
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ liveTest bool
+ apiEndpoint string
+ applicationKey string
+ applicationSecret string
+ consumerKey string
+ domain string
+)
+
+func init() {
+ apiEndpoint = os.Getenv("OVH_ENDPOINT")
+ applicationKey = os.Getenv("OVH_APPLICATION_KEY")
+ applicationSecret = os.Getenv("OVH_APPLICATION_SECRET")
+ consumerKey = os.Getenv("OVH_CONSUMER_KEY")
+ liveTest = len(apiEndpoint) > 0 && len(applicationKey) > 0 && len(applicationSecret) > 0 && len(consumerKey) > 0
+}
+
+func restoreEnv() {
+ os.Setenv("OVH_ENDPOINT", apiEndpoint)
+ os.Setenv("OVH_APPLICATION_KEY", applicationKey)
+ os.Setenv("OVH_APPLICATION_SECRET", applicationSecret)
+ os.Setenv("OVH_CONSUMER_KEY", consumerKey)
+}
+
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ os.Setenv("OVH_ENDPOINT", "ovh-eu")
+ os.Setenv("OVH_APPLICATION_KEY", "1234")
+ os.Setenv("OVH_APPLICATION_SECRET", "5678")
+ os.Setenv("OVH_CONSUMER_KEY", "abcde")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+}
+
+func TestNewDNSProviderMissingCredErr(t *testing.T) {
+ os.Setenv("OVH_ENDPOINT", "")
+ os.Setenv("OVH_APPLICATION_KEY", "1234")
+ os.Setenv("OVH_APPLICATION_SECRET", "5678")
+ os.Setenv("OVH_CONSUMER_KEY", "abcde")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "OVH credentials missing")
+
+ os.Setenv("OVH_ENDPOINT", "ovh-eu")
+ os.Setenv("OVH_APPLICATION_KEY", "")
+ os.Setenv("OVH_APPLICATION_SECRET", "5678")
+ os.Setenv("OVH_CONSUMER_KEY", "abcde")
+ defer restoreEnv()
+ _, err = NewDNSProvider()
+ assert.EqualError(t, err, "OVH credentials missing")
+
+ os.Setenv("OVH_ENDPOINT", "ovh-eu")
+ os.Setenv("OVH_APPLICATION_KEY", "1234")
+ os.Setenv("OVH_APPLICATION_SECRET", "")
+ os.Setenv("OVH_CONSUMER_KEY", "abcde")
+ defer restoreEnv()
+ _, err = NewDNSProvider()
+ assert.EqualError(t, err, "OVH credentials missing")
+
+ os.Setenv("OVH_ENDPOINT", "ovh-eu")
+ os.Setenv("OVH_APPLICATION_KEY", "1234")
+ os.Setenv("OVH_APPLICATION_SECRET", "5678")
+ os.Setenv("OVH_CONSUMER_KEY", "")
+ defer restoreEnv()
+ _, err = NewDNSProvider()
+ assert.EqualError(t, err, "OVH credentials missing")
+}
+
+func TestLivePresent(t *testing.T) {
+ if !liveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProvider()
+ 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 := NewDNSProvider()
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(domain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/pdns/README.md b/vendor/github.com/xenolf/lego/providers/dns/pdns/README.md
new file mode 100644
index 000000000..23abb7669
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/pdns/README.md
@@ -0,0 +1,7 @@
+## PowerDNS provider
+
+Tested and confirmed to work with PowerDNS authoratative server 3.4.8 and 4.0.1. Refer to [PowerDNS documentation](https://doc.powerdns.com/md/httpapi/README/) instructions on how to enable the built-in API interface.
+
+PowerDNS Notes:
+- PowerDNS API does not currently support SSL, therefore you should take care to ensure that traffic between lego and the PowerDNS API is over a trusted network, VPN etc.
+- In order to have the SOA serial automatically increment each time the `_acme-challenge` record is added/modified via the API, set `SOA-API-EDIT` to `INCEPTION-INCREMENT` for the zone in the `domainmetadata` table
diff --git a/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns.go b/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns.go
new file mode 100644
index 000000000..a4fd22b0c
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns.go
@@ -0,0 +1,343 @@
+// Package pdns implements a DNS provider for solving the DNS-01
+// challenge using PowerDNS nameserver.
+package pdns
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface
+type DNSProvider struct {
+ apiKey string
+ host *url.URL
+ apiVersion int
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for pdns.
+// Credentials must be passed in the environment variable:
+// PDNS_API_URL and PDNS_API_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+ key := os.Getenv("PDNS_API_KEY")
+ hostUrl, err := url.Parse(os.Getenv("PDNS_API_URL"))
+ if err != nil {
+ return nil, err
+ }
+
+ return NewDNSProviderCredentials(hostUrl, key)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for pdns.
+func NewDNSProviderCredentials(host *url.URL, key string) (*DNSProvider, error) {
+ if key == "" {
+ return nil, fmt.Errorf("PDNS API key missing")
+ }
+
+ if host == nil || host.Host == "" {
+ return nil, fmt.Errorf("PDNS API URL missing")
+ }
+
+ provider := &DNSProvider{
+ host: host,
+ apiKey: key,
+ }
+ provider.getAPIVersion()
+
+ return provider, nil
+}
+
+// Timeout returns the timeout and interval to use when checking for DNS
+// propagation. Adjusting here to cope with spikes in propagation times.
+func (c *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return 120 * time.Second, 2 * time.Second
+}
+
+// Present creates a TXT record to fulfil the dns-01 challenge
+func (c *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
+ zone, err := c.getHostedZone(fqdn)
+ if err != nil {
+ return err
+ }
+
+ name := fqdn
+
+ // pre-v1 API wants non-fqdn
+ if c.apiVersion == 0 {
+ name = acme.UnFqdn(fqdn)
+ }
+
+ rec := pdnsRecord{
+ Content: "\"" + value + "\"",
+ Disabled: false,
+
+ // pre-v1 API
+ Type: "TXT",
+ Name: name,
+ TTL: 120,
+ }
+
+ rrsets := rrSets{
+ RRSets: []rrSet{
+ rrSet{
+ Name: name,
+ ChangeType: "REPLACE",
+ Type: "TXT",
+ Kind: "Master",
+ TTL: 120,
+ Records: []pdnsRecord{rec},
+ },
+ },
+ }
+
+ body, err := json.Marshal(rrsets)
+ if err != nil {
+ return err
+ }
+
+ _, err = c.makeRequest("PATCH", zone.URL, bytes.NewReader(body))
+ if err != nil {
+ fmt.Println("here")
+ 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(fqdn)
+ if err != nil {
+ return err
+ }
+
+ set, err := c.findTxtRecord(fqdn)
+ if err != nil {
+ return err
+ }
+
+ rrsets := rrSets{
+ RRSets: []rrSet{
+ rrSet{
+ Name: set.Name,
+ Type: set.Type,
+ ChangeType: "DELETE",
+ },
+ },
+ }
+ body, err := json.Marshal(rrsets)
+ if err != nil {
+ return err
+ }
+
+ _, err = c.makeRequest("PATCH", zone.URL, bytes.NewReader(body))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) {
+ var zone hostedZone
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return nil, err
+ }
+
+ url := "/servers/localhost/zones"
+ result, err := c.makeRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ zones := []hostedZone{}
+ err = json.Unmarshal(result, &zones)
+ if err != nil {
+ return nil, err
+ }
+
+ url = ""
+ for _, zone := range zones {
+ if acme.UnFqdn(zone.Name) == acme.UnFqdn(authZone) {
+ url = zone.URL
+ }
+ }
+
+ result, err = c.makeRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ err = json.Unmarshal(result, &zone)
+ if err != nil {
+ return nil, err
+ }
+
+ // convert pre-v1 API result
+ if len(zone.Records) > 0 {
+ zone.RRSets = []rrSet{}
+ for _, record := range zone.Records {
+ set := rrSet{
+ Name: record.Name,
+ Type: record.Type,
+ Records: []pdnsRecord{record},
+ }
+ zone.RRSets = append(zone.RRSets, set)
+ }
+ }
+
+ return &zone, nil
+}
+
+func (c *DNSProvider) findTxtRecord(fqdn string) (*rrSet, error) {
+ zone, err := c.getHostedZone(fqdn)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = c.makeRequest("GET", zone.URL, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, set := range zone.RRSets {
+ if (set.Name == acme.UnFqdn(fqdn) || set.Name == fqdn) && set.Type == "TXT" {
+ return &set, nil
+ }
+ }
+
+ return nil, fmt.Errorf("No existing record found for %s", fqdn)
+}
+
+func (c *DNSProvider) getAPIVersion() {
+ type APIVersion struct {
+ URL string `json:"url"`
+ Version int `json:"version"`
+ }
+
+ result, err := c.makeRequest("GET", "/api", nil)
+ if err != nil {
+ return
+ }
+
+ var versions []APIVersion
+ err = json.Unmarshal(result, &versions)
+ if err != nil {
+ return
+ }
+
+ latestVersion := 0
+ for _, v := range versions {
+ if v.Version > latestVersion {
+ latestVersion = v.Version
+ }
+ }
+ c.apiVersion = latestVersion
+}
+
+func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
+ type APIError struct {
+ Error string `json:"error"`
+ }
+ var path = ""
+ if c.host.Path != "/" {
+ path = c.host.Path
+ }
+ if c.apiVersion > 0 {
+ if !strings.HasPrefix(uri, "api/v") {
+ uri = "/api/v" + strconv.Itoa(c.apiVersion) + uri
+ } else {
+ uri = "/" + uri
+ }
+ }
+ url := c.host.Scheme + "://" + c.host.Host + path + uri
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("X-API-Key", c.apiKey)
+
+ client := http.Client{Timeout: 30 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("Error talking to PDNS API -> %v", err)
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 422 && (resp.StatusCode < 200 || resp.StatusCode >= 300) {
+ return nil, fmt.Errorf("Unexpected HTTP status code %d when fetching '%s'", resp.StatusCode, url)
+ }
+
+ var msg json.RawMessage
+ err = json.NewDecoder(resp.Body).Decode(&msg)
+ switch {
+ case err == io.EOF:
+ // empty body
+ return nil, nil
+ case err != nil:
+ // other error
+ return nil, err
+ }
+
+ // check for PowerDNS error message
+ if len(msg) > 0 && msg[0] == '{' {
+ var apiError APIError
+ err = json.Unmarshal(msg, &apiError)
+ if err != nil {
+ return nil, err
+ }
+ if apiError.Error != "" {
+ return nil, fmt.Errorf("Error talking to PDNS API -> %v", apiError.Error)
+ }
+ }
+ return msg, nil
+}
+
+type pdnsRecord struct {
+ Content string `json:"content"`
+ Disabled bool `json:"disabled"`
+
+ // pre-v1 API
+ Name string `json:"name"`
+ Type string `json:"type"`
+ TTL int `json:"ttl,omitempty"`
+}
+
+type hostedZone struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ URL string `json:"url"`
+ RRSets []rrSet `json:"rrsets"`
+
+ // pre-v1 API
+ Records []pdnsRecord `json:"records"`
+}
+
+type rrSet struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Kind string `json:"kind"`
+ ChangeType string `json:"changetype"`
+ Records []pdnsRecord `json:"records"`
+ TTL int `json:"ttl,omitempty"`
+}
+
+type rrSets struct {
+ RRSets []rrSet `json:"rrsets"`
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns_test.go b/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns_test.go
new file mode 100644
index 000000000..70e7670ed
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/pdns/pdns_test.go
@@ -0,0 +1,80 @@
+package pdns
+
+import (
+ "net/url"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ pdnsLiveTest bool
+ pdnsURL *url.URL
+ pdnsURLStr string
+ pdnsAPIKey string
+ pdnsDomain string
+)
+
+func init() {
+ pdnsURLStr = os.Getenv("PDNS_API_URL")
+ pdnsURL, _ = url.Parse(pdnsURLStr)
+ pdnsAPIKey = os.Getenv("PDNS_API_KEY")
+ pdnsDomain = os.Getenv("PDNS_DOMAIN")
+ if len(pdnsURLStr) > 0 && len(pdnsAPIKey) > 0 && len(pdnsDomain) > 0 {
+ pdnsLiveTest = true
+ }
+}
+
+func restorePdnsEnv() {
+ os.Setenv("PDNS_API_URL", pdnsURLStr)
+ os.Setenv("PDNS_API_KEY", pdnsAPIKey)
+}
+
+func TestNewDNSProviderValid(t *testing.T) {
+ os.Setenv("PDNS_API_URL", "")
+ os.Setenv("PDNS_API_KEY", "")
+ tmpURL, _ := url.Parse("http://localhost:8081")
+ _, err := NewDNSProviderCredentials(tmpURL, "123")
+ assert.NoError(t, err)
+ restorePdnsEnv()
+}
+
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ os.Setenv("PDNS_API_URL", "http://localhost:8081")
+ os.Setenv("PDNS_API_KEY", "123")
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+ restorePdnsEnv()
+}
+
+func TestNewDNSProviderMissingHostErr(t *testing.T) {
+ os.Setenv("PDNS_API_URL", "")
+ os.Setenv("PDNS_API_KEY", "123")
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "PDNS API URL missing")
+ restorePdnsEnv()
+}
+
+func TestNewDNSProviderMissingKeyErr(t *testing.T) {
+ os.Setenv("PDNS_API_URL", pdnsURLStr)
+ os.Setenv("PDNS_API_KEY", "")
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "PDNS API key missing")
+ restorePdnsEnv()
+}
+
+func TestPdnsPresentAndCleanup(t *testing.T) {
+ if !pdnsLiveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProviderCredentials(pdnsURL, pdnsAPIKey)
+ assert.NoError(t, err)
+
+ err = provider.Present(pdnsDomain, "", "123d==")
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(pdnsDomain, "", "123d==")
+ assert.NoError(t, err)
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136.go b/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136.go
new file mode 100644
index 000000000..43a95f18c
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136.go
@@ -0,0 +1,129 @@
+// Package rfc2136 implements a DNS provider for solving the DNS-01 challenge
+// using the rfc2136 dynamic update.
+package rfc2136
+
+import (
+ "fmt"
+ "net"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/miekg/dns"
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface that
+// uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver.
+type DNSProvider struct {
+ nameserver string
+ tsigAlgorithm string
+ tsigKey string
+ tsigSecret string
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for rfc2136
+// dynamic update. Credentials must be passed in the environment variables:
+// RFC2136_NAMESERVER, RFC2136_TSIG_ALGORITHM, RFC2136_TSIG_KEY and
+// RFC2136_TSIG_SECRET. To disable TSIG authentication, leave the TSIG
+// variables unset. RFC2136_NAMESERVER must be a network address in the form
+// "host" or "host:port".
+func NewDNSProvider() (*DNSProvider, error) {
+ nameserver := os.Getenv("RFC2136_NAMESERVER")
+ tsigAlgorithm := os.Getenv("RFC2136_TSIG_ALGORITHM")
+ tsigKey := os.Getenv("RFC2136_TSIG_KEY")
+ tsigSecret := os.Getenv("RFC2136_TSIG_SECRET")
+ return NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a
+// DNSProvider instance configured for rfc2136 dynamic update. To disable TSIG
+// authentication, leave the TSIG parameters as empty strings.
+// nameserver must be a network address in the form "host" or "host:port".
+func NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProvider, error) {
+ if nameserver == "" {
+ return nil, fmt.Errorf("RFC2136 nameserver missing")
+ }
+
+ // Append the default DNS port if none is specified.
+ if _, _, err := net.SplitHostPort(nameserver); err != nil {
+ if strings.Contains(err.Error(), "missing port") {
+ nameserver = net.JoinHostPort(nameserver, "53")
+ } else {
+ return nil, err
+ }
+ }
+ d := &DNSProvider{
+ nameserver: nameserver,
+ }
+ if tsigAlgorithm == "" {
+ tsigAlgorithm = dns.HmacMD5
+ }
+ d.tsigAlgorithm = tsigAlgorithm
+ if len(tsigKey) > 0 && len(tsigSecret) > 0 {
+ d.tsigKey = tsigKey
+ d.tsigSecret = tsigSecret
+ }
+
+ return d, nil
+}
+
+// Present creates a TXT record using the specified parameters
+func (r *DNSProvider) Present(domain, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+ return r.changeRecord("INSERT", fqdn, value, ttl)
+}
+
+// CleanUp removes the TXT record matching the specified parameters
+func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
+ return r.changeRecord("REMOVE", fqdn, value, ttl)
+}
+
+func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
+ // Find the zone for the given fqdn
+ zone, err := acme.FindZoneByFqdn(fqdn, []string{r.nameserver})
+ if err != nil {
+ return err
+ }
+
+ // Create RR
+ rr := new(dns.TXT)
+ rr.Hdr = dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)}
+ rr.Txt = []string{value}
+ rrs := []dns.RR{rr}
+
+ // Create dynamic update packet
+ m := new(dns.Msg)
+ m.SetUpdate(zone)
+ switch action {
+ case "INSERT":
+ // Always remove old challenge left over from who knows what.
+ m.RemoveRRset(rrs)
+ m.Insert(rrs)
+ case "REMOVE":
+ m.Remove(rrs)
+ default:
+ return fmt.Errorf("Unexpected action: %s", action)
+ }
+
+ // Setup client
+ c := new(dns.Client)
+ c.SingleInflight = true
+ // TSIG authentication / msg signing
+ if len(r.tsigKey) > 0 && len(r.tsigSecret) > 0 {
+ m.SetTsig(dns.Fqdn(r.tsigKey), r.tsigAlgorithm, 300, time.Now().Unix())
+ c.TsigSecret = map[string]string{dns.Fqdn(r.tsigKey): r.tsigSecret}
+ }
+
+ // Send the query
+ reply, _, err := c.Exchange(m, r.nameserver)
+ if err != nil {
+ return fmt.Errorf("DNS update failed: %v", err)
+ }
+ if reply != nil && reply.Rcode != dns.RcodeSuccess {
+ return fmt.Errorf("DNS update failed. Server replied: %s", dns.RcodeToString[reply.Rcode])
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136_test.go b/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136_test.go
new file mode 100644
index 000000000..a2515e995
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/rfc2136/rfc2136_test.go
@@ -0,0 +1,244 @@
+package rfc2136
+
+import (
+ "bytes"
+ "fmt"
+ "net"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/miekg/dns"
+ "github.com/xenolf/lego/acme"
+)
+
+var (
+ rfc2136TestDomain = "123456789.www.example.com"
+ rfc2136TestKeyAuth = "123d=="
+ rfc2136TestValue = "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo"
+ rfc2136TestFqdn = "_acme-challenge.123456789.www.example.com."
+ rfc2136TestZone = "example.com."
+ rfc2136TestTTL = 120
+ rfc2136TestTsigKey = "example.com."
+ rfc2136TestTsigSecret = "IwBTJx9wrDp4Y1RyC3H0gA=="
+)
+
+var reqChan = make(chan *dns.Msg, 10)
+
+func TestRFC2136CanaryLocalTestServer(t *testing.T) {
+ acme.ClearFqdnCache()
+ dns.HandleFunc("example.com.", serverHandlerHello)
+ defer dns.HandleRemove("example.com.")
+
+ server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
+ if err != nil {
+ t.Fatalf("Failed to start test server: %v", err)
+ }
+ defer server.Shutdown()
+
+ c := new(dns.Client)
+ m := new(dns.Msg)
+ m.SetQuestion("example.com.", dns.TypeTXT)
+ r, _, err := c.Exchange(m, addrstr)
+ if err != nil || len(r.Extra) == 0 {
+ t.Fatalf("Failed to communicate with test server: %v", err)
+ }
+ txt := r.Extra[0].(*dns.TXT).Txt[0]
+ if txt != "Hello world" {
+ t.Error("Expected test server to return 'Hello world' but got: ", txt)
+ }
+}
+
+func TestRFC2136ServerSuccess(t *testing.T) {
+ acme.ClearFqdnCache()
+ dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess)
+ defer dns.HandleRemove(rfc2136TestZone)
+
+ server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
+ if err != nil {
+ t.Fatalf("Failed to start test server: %v", err)
+ }
+ defer server.Shutdown()
+
+ provider, err := NewDNSProviderCredentials(addrstr, "", "", "")
+ if err != nil {
+ t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err)
+ }
+ if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil {
+ t.Errorf("Expected Present() to return no error but the error was -> %v", err)
+ }
+}
+
+func TestRFC2136ServerError(t *testing.T) {
+ acme.ClearFqdnCache()
+ dns.HandleFunc(rfc2136TestZone, serverHandlerReturnErr)
+ defer dns.HandleRemove(rfc2136TestZone)
+
+ server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
+ if err != nil {
+ t.Fatalf("Failed to start test server: %v", err)
+ }
+ defer server.Shutdown()
+
+ provider, err := NewDNSProviderCredentials(addrstr, "", "", "")
+ if err != nil {
+ t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err)
+ }
+ if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err == nil {
+ t.Errorf("Expected Present() to return an error but it did not.")
+ } else if !strings.Contains(err.Error(), "NOTZONE") {
+ t.Errorf("Expected Present() to return an error with the 'NOTZONE' rcode string but it did not.")
+ }
+}
+
+func TestRFC2136TsigClient(t *testing.T) {
+ acme.ClearFqdnCache()
+ dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess)
+ defer dns.HandleRemove(rfc2136TestZone)
+
+ server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", true)
+ if err != nil {
+ t.Fatalf("Failed to start test server: %v", err)
+ }
+ defer server.Shutdown()
+
+ provider, err := NewDNSProviderCredentials(addrstr, "", rfc2136TestTsigKey, rfc2136TestTsigSecret)
+ if err != nil {
+ t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err)
+ }
+ if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil {
+ t.Errorf("Expected Present() to return no error but the error was -> %v", err)
+ }
+}
+
+func TestRFC2136ValidUpdatePacket(t *testing.T) {
+ acme.ClearFqdnCache()
+ dns.HandleFunc(rfc2136TestZone, serverHandlerPassBackRequest)
+ defer dns.HandleRemove(rfc2136TestZone)
+
+ server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
+ if err != nil {
+ t.Fatalf("Failed to start test server: %v", err)
+ }
+ defer server.Shutdown()
+
+ txtRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN TXT %s", rfc2136TestFqdn, rfc2136TestTTL, rfc2136TestValue))
+ rrs := []dns.RR{txtRR}
+ m := new(dns.Msg)
+ m.SetUpdate(rfc2136TestZone)
+ m.RemoveRRset(rrs)
+ m.Insert(rrs)
+ expectstr := m.String()
+ expect, err := m.Pack()
+ if err != nil {
+ t.Fatalf("Error packing expect msg: %v", err)
+ }
+
+ provider, err := NewDNSProviderCredentials(addrstr, "", "", "")
+ if err != nil {
+ t.Fatalf("Expected NewDNSProviderCredentials() to return no error but the error was -> %v", err)
+ }
+
+ if err := provider.Present(rfc2136TestDomain, "", "1234d=="); err != nil {
+ t.Errorf("Expected Present() to return no error but the error was -> %v", err)
+ }
+
+ rcvMsg := <-reqChan
+ rcvMsg.Id = m.Id
+ actual, err := rcvMsg.Pack()
+ if err != nil {
+ t.Fatalf("Error packing actual msg: %v", err)
+ }
+
+ if !bytes.Equal(actual, expect) {
+ tmp := new(dns.Msg)
+ if err := tmp.Unpack(actual); err != nil {
+ t.Fatalf("Error unpacking actual msg: %v", err)
+ }
+ t.Errorf("Expected msg:\n%s", expectstr)
+ t.Errorf("Actual msg:\n%v", tmp)
+ }
+}
+
+func runLocalDNSTestServer(listenAddr string, tsig bool) (*dns.Server, string, error) {
+ pc, err := net.ListenPacket("udp", listenAddr)
+ if err != nil {
+ return nil, "", err
+ }
+ server := &dns.Server{PacketConn: pc, ReadTimeout: time.Hour, WriteTimeout: time.Hour}
+ if tsig {
+ server.TsigSecret = map[string]string{rfc2136TestTsigKey: rfc2136TestTsigSecret}
+ }
+
+ waitLock := sync.Mutex{}
+ waitLock.Lock()
+ server.NotifyStartedFunc = waitLock.Unlock
+
+ go func() {
+ server.ActivateAndServe()
+ pc.Close()
+ }()
+
+ waitLock.Lock()
+ return server, pc.LocalAddr().String(), nil
+}
+
+func serverHandlerHello(w dns.ResponseWriter, req *dns.Msg) {
+ m := new(dns.Msg)
+ m.SetReply(req)
+ m.Extra = make([]dns.RR, 1)
+ m.Extra[0] = &dns.TXT{
+ Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0},
+ Txt: []string{"Hello world"},
+ }
+ w.WriteMsg(m)
+}
+
+func serverHandlerReturnSuccess(w dns.ResponseWriter, req *dns.Msg) {
+ m := new(dns.Msg)
+ m.SetReply(req)
+ if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET {
+ // Return SOA to appease findZoneByFqdn()
+ soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", rfc2136TestZone, rfc2136TestTTL, rfc2136TestZone, rfc2136TestZone))
+ m.Answer = []dns.RR{soaRR}
+ }
+
+ if t := req.IsTsig(); t != nil {
+ if w.TsigStatus() == nil {
+ // Validated
+ m.SetTsig(rfc2136TestZone, dns.HmacMD5, 300, time.Now().Unix())
+ }
+ }
+
+ w.WriteMsg(m)
+}
+
+func serverHandlerReturnErr(w dns.ResponseWriter, req *dns.Msg) {
+ m := new(dns.Msg)
+ m.SetRcode(req, dns.RcodeNotZone)
+ w.WriteMsg(m)
+}
+
+func serverHandlerPassBackRequest(w dns.ResponseWriter, req *dns.Msg) {
+ m := new(dns.Msg)
+ m.SetReply(req)
+ if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET {
+ // Return SOA to appease findZoneByFqdn()
+ soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", rfc2136TestZone, rfc2136TestTTL, rfc2136TestZone, rfc2136TestZone))
+ m.Answer = []dns.RR{soaRR}
+ }
+
+ if t := req.IsTsig(); t != nil {
+ if w.TsigStatus() == nil {
+ // Validated
+ m.SetTsig(rfc2136TestZone, dns.HmacMD5, 300, time.Now().Unix())
+ }
+ }
+
+ w.WriteMsg(m)
+ if req.Opcode != dns.OpcodeQuery || req.Question[0].Qtype != dns.TypeSOA || req.Question[0].Qclass != dns.ClassINET {
+ // Only talk back when it is not the SOA RR.
+ reqChan <- req
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/route53/fixtures_test.go b/vendor/github.com/xenolf/lego/providers/dns/route53/fixtures_test.go
new file mode 100644
index 000000000..a5cc9c878
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/route53/fixtures_test.go
@@ -0,0 +1,39 @@
+package route53
+
+var ChangeResourceRecordSetsResponse = `<?xml version="1.0" encoding="UTF-8"?>
+<ChangeResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
+<ChangeInfo>
+ <Id>/change/123456</Id>
+ <Status>PENDING</Status>
+ <SubmittedAt>2016-02-10T01:36:41.958Z</SubmittedAt>
+</ChangeInfo>
+</ChangeResourceRecordSetsResponse>`
+
+var ListHostedZonesByNameResponse = `<?xml version="1.0" encoding="UTF-8"?>
+<ListHostedZonesByNameResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
+ <HostedZones>
+ <HostedZone>
+ <Id>/hostedzone/ABCDEFG</Id>
+ <Name>example.com.</Name>
+ <CallerReference>D2224C5B-684A-DB4A-BB9A-E09E3BAFEA7A</CallerReference>
+ <Config>
+ <Comment>Test comment</Comment>
+ <PrivateZone>false</PrivateZone>
+ </Config>
+ <ResourceRecordSetCount>10</ResourceRecordSetCount>
+ </HostedZone>
+ </HostedZones>
+ <IsTruncated>true</IsTruncated>
+ <NextDNSName>example2.com</NextDNSName>
+ <NextHostedZoneId>ZLT12321321124</NextHostedZoneId>
+ <MaxItems>1</MaxItems>
+</ListHostedZonesByNameResponse>`
+
+var GetChangeResponse = `<?xml version="1.0" encoding="UTF-8"?>
+<GetChangeResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
+ <ChangeInfo>
+ <Id>123456</Id>
+ <Status>INSYNC</Status>
+ <SubmittedAt>2016-02-10T01:36:41.958Z</SubmittedAt>
+ </ChangeInfo>
+</GetChangeResponse>`
diff --git a/vendor/github.com/xenolf/lego/providers/dns/route53/route53.go b/vendor/github.com/xenolf/lego/providers/dns/route53/route53.go
new file mode 100644
index 000000000..f3e53a8e5
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/route53/route53.go
@@ -0,0 +1,171 @@
+// Package route53 implements a DNS provider for solving the DNS-01 challenge
+// using AWS Route 53 DNS.
+package route53
+
+import (
+ "fmt"
+ "math/rand"
+ "strings"
+ "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/route53"
+ "github.com/xenolf/lego/acme"
+)
+
+const (
+ maxRetries = 5
+ route53TTL = 10
+)
+
+// DNSProvider implements the acme.ChallengeProvider interface
+type DNSProvider struct {
+ client *route53.Route53
+}
+
+// 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
+// Route 53 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_REGION, [AWS_SESSION_TOKEN]
+// 2. Shared credentials file (defaults to ~/.aws/credentials)
+// 3. Amazon EC2 IAM role
+//
+// 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 := route53.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 + `"`
+ return r.changeRecord("UPSERT", fqdn, value, route53TTL)
+}
+
+// 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 + `"`
+ return r.changeRecord("DELETE", fqdn, value, route53TTL)
+}
+
+func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
+ hostedZoneID, err := getHostedZoneID(fqdn, r.client)
+ if err != nil {
+ return fmt.Errorf("Failed to determine Route 53 hosted zone ID: %v", err)
+ }
+
+ recordSet := newTXTRecordSet(fqdn, value, ttl)
+ reqParams := &route53.ChangeResourceRecordSetsInput{
+ HostedZoneId: aws.String(hostedZoneID),
+ ChangeBatch: &route53.ChangeBatch{
+ Comment: aws.String("Managed by Lego"),
+ Changes: []*route53.Change{
+ {
+ Action: aws.String(action),
+ ResourceRecordSet: recordSet,
+ },
+ },
+ },
+ }
+
+ resp, err := r.client.ChangeResourceRecordSets(reqParams)
+ if err != nil {
+ return fmt.Errorf("Failed to change Route 53 record set: %v", err)
+ }
+
+ statusID := resp.ChangeInfo.Id
+
+ return acme.WaitFor(120*time.Second, 4*time.Second, func() (bool, error) {
+ reqParams := &route53.GetChangeInput{
+ Id: statusID,
+ }
+ resp, err := r.client.GetChange(reqParams)
+ if err != nil {
+ return false, fmt.Errorf("Failed to query Route 53 change status: %v", err)
+ }
+ if *resp.ChangeInfo.Status == route53.ChangeStatusInsync {
+ return true, nil
+ }
+ return false, nil
+ })
+}
+
+func getHostedZoneID(fqdn string, client *route53.Route53) (string, error) {
+ authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+ if err != nil {
+ return "", err
+ }
+
+ // .DNSName should not have a trailing dot
+ reqParams := &route53.ListHostedZonesByNameInput{
+ DNSName: aws.String(acme.UnFqdn(authZone)),
+ }
+ resp, err := client.ListHostedZonesByName(reqParams)
+ if err != nil {
+ return "", err
+ }
+
+ var hostedZoneID string
+ for _, hostedZone := range resp.HostedZones {
+ // .Name has a trailing dot
+ if !*hostedZone.Config.PrivateZone && *hostedZone.Name == authZone {
+ hostedZoneID = *hostedZone.Id
+ break
+ }
+ }
+
+ if len(hostedZoneID) == 0 {
+ return "", fmt.Errorf("Zone %s not found in Route 53 for domain %s", authZone, fqdn)
+ }
+
+ if strings.HasPrefix(hostedZoneID, "/hostedzone/") {
+ hostedZoneID = strings.TrimPrefix(hostedZoneID, "/hostedzone/")
+ }
+
+ return hostedZoneID, nil
+}
+
+func newTXTRecordSet(fqdn, value string, ttl int) *route53.ResourceRecordSet {
+ return &route53.ResourceRecordSet{
+ Name: aws.String(fqdn),
+ Type: aws.String("TXT"),
+ TTL: aws.Int64(int64(ttl)),
+ ResourceRecords: []*route53.ResourceRecord{
+ {Value: aws.String(value)},
+ },
+ }
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/route53/route53_integration_test.go b/vendor/github.com/xenolf/lego/providers/dns/route53/route53_integration_test.go
new file mode 100644
index 000000000..64678906a
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/route53/route53_integration_test.go
@@ -0,0 +1,70 @@
+package route53
+
+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/route53"
+)
+
+func TestRoute53TTL(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["route53Domain"], "foo", "bar")
+ if err != nil {
+ t.Fatalf("Fatal: %s", err.Error())
+ }
+ // we need a separate R53 client here as the one in the DNS provider is
+ // unexported.
+ fqdn := "_acme-challenge." + m["route53Domain"] + "."
+ svc := route53.New(session.New())
+ zoneID, err := getHostedZoneID(fqdn, svc)
+ if err != nil {
+ provider.CleanUp(m["route53Domain"], "foo", "bar")
+ t.Fatalf("Fatal: %s", err.Error())
+ }
+ params := &route53.ListResourceRecordSetsInput{
+ HostedZoneId: aws.String(zoneID),
+ }
+ resp, err := svc.ListResourceRecordSets(params)
+ if err != nil {
+ provider.CleanUp(m["route53Domain"], "foo", "bar")
+ t.Fatalf("Fatal: %s", err.Error())
+ }
+
+ for _, v := range resp.ResourceRecordSets {
+ if *v.Name == fqdn && *v.Type == "TXT" && *v.TTL == 10 {
+ provider.CleanUp(m["route53Domain"], "foo", "bar")
+ return
+ }
+ }
+ provider.CleanUp(m["route53Domain"], "foo", "bar")
+ t.Fatalf("Could not find a TXT record for _acme-challenge.%s with a TTL of 10", m["route53Domain"])
+}
+
+func testGetAndPreCheck() (map[string]string, error) {
+ m := map[string]string{
+ "route53Key": os.Getenv("AWS_ACCESS_KEY_ID"),
+ "route53Secret": os.Getenv("AWS_SECRET_ACCESS_KEY"),
+ "route53Region": os.Getenv("AWS_REGION"),
+ "route53Domain": os.Getenv("R53_DOMAIN"),
+ }
+ for _, v := range m {
+ if v == "" {
+ return nil, fmt.Errorf("AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, and R53_DOMAIN are needed to run this test")
+ }
+ }
+ return m, nil
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/route53/route53_test.go b/vendor/github.com/xenolf/lego/providers/dns/route53/route53_test.go
new file mode 100644
index 000000000..ab8739a58
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/route53/route53_test.go
@@ -0,0 +1,87 @@
+package route53
+
+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/route53"
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ route53Secret string
+ route53Key string
+ route53Region string
+)
+
+func init() {
+ route53Key = os.Getenv("AWS_ACCESS_KEY_ID")
+ route53Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
+ route53Region = os.Getenv("AWS_REGION")
+}
+
+func restoreRoute53Env() {
+ os.Setenv("AWS_ACCESS_KEY_ID", route53Key)
+ os.Setenv("AWS_SECRET_ACCESS_KEY", route53Secret)
+ os.Setenv("AWS_REGION", route53Region)
+}
+
+func makeRoute53Provider(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 := route53.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")
+
+ restoreRoute53Env()
+}
+
+func TestRegionFromEnv(t *testing.T) {
+ os.Setenv("AWS_REGION", "us-east-1")
+
+ sess := session.New(aws.NewConfig())
+ assert.Equal(t, "us-east-1", *sess.Config.Region, "Expected Region to be set from environment")
+
+ restoreRoute53Env()
+}
+
+func TestRoute53Present(t *testing.T) {
+ mockResponses := MockResponseMap{
+ "/2013-04-01/hostedzonesbyname": MockResponse{StatusCode: 200, Body: ListHostedZonesByNameResponse},
+ "/2013-04-01/hostedzone/ABCDEFG/rrset/": MockResponse{StatusCode: 200, Body: ChangeResourceRecordSetsResponse},
+ "/2013-04-01/change/123456": MockResponse{StatusCode: 200, Body: GetChangeResponse},
+ }
+
+ ts := newMockServer(t, mockResponses)
+ defer ts.Close()
+
+ provider := makeRoute53Provider(ts)
+
+ domain := "example.com"
+ keyAuth := "123456d=="
+
+ err := provider.Present(domain, "", keyAuth)
+ assert.NoError(t, err, "Expected Present to return no error")
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/route53/testutil_test.go b/vendor/github.com/xenolf/lego/providers/dns/route53/testutil_test.go
new file mode 100644
index 000000000..e448a6858
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/route53/testutil_test.go
@@ -0,0 +1,38 @@
+package route53
+
+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
+}
diff --git a/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr.go b/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr.go
new file mode 100644
index 000000000..53804e270
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr.go
@@ -0,0 +1,127 @@
+// Package vultr implements a DNS provider for solving the DNS-01 challenge using
+// the vultr DNS.
+// See https://www.vultr.com/api/#dns
+package vultr
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ vultr "github.com/JamesClonk/vultr/lib"
+ "github.com/xenolf/lego/acme"
+)
+
+// DNSProvider is an implementation of the acme.ChallengeProvider interface.
+type DNSProvider struct {
+ client *vultr.Client
+}
+
+// NewDNSProvider returns a DNSProvider instance with a configured Vultr client.
+// Authentication uses the VULTR_API_KEY environment variable.
+func NewDNSProvider() (*DNSProvider, error) {
+ apiKey := os.Getenv("VULTR_API_KEY")
+ return NewDNSProviderCredentials(apiKey)
+}
+
+// NewDNSProviderCredentials uses the supplied credentials to return a DNSProvider
+// instance configured for Vultr.
+func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) {
+ if apiKey == "" {
+ return nil, fmt.Errorf("Vultr credentials missing")
+ }
+
+ c := &DNSProvider{
+ client: vultr.NewClient(apiKey, nil),
+ }
+
+ return c, 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)
+
+ zoneDomain, err := c.getHostedZone(domain)
+ if err != nil {
+ return err
+ }
+
+ name := c.extractRecordName(fqdn, zoneDomain)
+
+ err = c.client.CreateDnsRecord(zoneDomain, name, "TXT", `"`+value+`"`, 0, ttl)
+ if err != nil {
+ return fmt.Errorf("Vultr 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)
+
+ zoneDomain, records, err := c.findTxtRecords(domain, fqdn)
+ if err != nil {
+ return err
+ }
+
+ for _, rec := range records {
+ err := c.client.DeleteDnsRecord(zoneDomain, rec.RecordID)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (c *DNSProvider) getHostedZone(domain string) (string, error) {
+ domains, err := c.client.GetDnsDomains()
+ if err != nil {
+ return "", fmt.Errorf("Vultr API call failed: %v", err)
+ }
+
+ var hostedDomain vultr.DnsDomain
+ for _, d := range domains {
+ if strings.HasSuffix(domain, d.Domain) {
+ if len(d.Domain) > len(hostedDomain.Domain) {
+ hostedDomain = d
+ }
+ }
+ }
+ if hostedDomain.Domain == "" {
+ return "", fmt.Errorf("No matching Vultr domain found for domain %s", domain)
+ }
+
+ return hostedDomain.Domain, nil
+}
+
+func (c *DNSProvider) findTxtRecords(domain, fqdn string) (string, []vultr.DnsRecord, error) {
+ zoneDomain, err := c.getHostedZone(domain)
+ if err != nil {
+ return "", nil, err
+ }
+
+ var records []vultr.DnsRecord
+ result, err := c.client.GetDnsRecords(zoneDomain)
+ if err != nil {
+ return "", records, fmt.Errorf("Vultr API call has failed: %v", err)
+ }
+
+ recordName := c.extractRecordName(fqdn, zoneDomain)
+ for _, record := range result {
+ if record.Type == "TXT" && record.Name == recordName {
+ records = append(records, record)
+ }
+ }
+
+ return zoneDomain, 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/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr_test.go b/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr_test.go
new file mode 100644
index 000000000..7c8cdaf1e
--- /dev/null
+++ b/vendor/github.com/xenolf/lego/providers/dns/vultr/vultr_test.go
@@ -0,0 +1,65 @@
+package vultr
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ liveTest bool
+ apiKey string
+ domain string
+)
+
+func init() {
+ apiKey = os.Getenv("VULTR_API_KEY")
+ domain = os.Getenv("VULTR_TEST_DOMAIN")
+ liveTest = len(apiKey) > 0 && len(domain) > 0
+}
+
+func restoreEnv() {
+ os.Setenv("VULTR_API_KEY", apiKey)
+}
+
+func TestNewDNSProviderValidEnv(t *testing.T) {
+ os.Setenv("VULTR_API_KEY", "123")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.NoError(t, err)
+}
+
+func TestNewDNSProviderMissingCredErr(t *testing.T) {
+ os.Setenv("VULTR_API_KEY", "")
+ defer restoreEnv()
+ _, err := NewDNSProvider()
+ assert.EqualError(t, err, "Vultr credentials missing")
+}
+
+func TestLivePresent(t *testing.T) {
+ if !liveTest {
+ t.Skip("skipping live test")
+ }
+
+ provider, err := NewDNSProvider()
+ 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 := NewDNSProvider()
+ assert.NoError(t, err)
+
+ err = provider.CleanUp(domain, "", "123d==")
+ assert.NoError(t, err)
+}