From 0135904f7d3e1c0e763adaefe267c736616e3d26 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Wed, 16 Nov 2016 19:28:52 -0500 Subject: Upgrading server dependancies (#4566) --- .../lego/providers/dns/auroradns/auroradns.go | 141 ++++++++++ .../lego/providers/dns/auroradns/auroradns_test.go | 148 +++++++++++ .../xenolf/lego/providers/dns/azure/azure.go | 142 +++++++++++ .../xenolf/lego/providers/dns/azure/azure_test.go | 89 +++++++ .../xenolf/lego/providers/dns/dnspod/dnspod.go | 146 +++++++++++ .../lego/providers/dns/dnspod/dnspod_test.go | 72 ++++++ .../xenolf/lego/providers/dns/exoscale/exoscale.go | 132 ++++++++++ .../lego/providers/dns/exoscale/exoscale_test.go | 103 ++++++++ .../xenolf/lego/providers/dns/gandi/gandi_test.go | 4 +- .../lego/providers/dns/googlecloud/googlecloud.go | 10 + .../providers/dns/googlecloud/googlecloud_test.go | 14 + .../xenolf/lego/providers/dns/ns1/ns1.go | 97 +++++++ .../xenolf/lego/providers/dns/ns1/ns1_test.go | 67 +++++ .../lego/providers/dns/rackspace/rackspace.go | 284 +++++++++++++++++++++ .../lego/providers/dns/rackspace/rackspace_test.go | 220 ++++++++++++++++ .../xenolf/lego/providers/http/memcached/README.md | 15 ++ .../lego/providers/http/memcached/memcached.go | 59 +++++ .../providers/http/memcached/memcached_test.go | 111 ++++++++ 18 files changed, 1852 insertions(+), 2 deletions(-) create mode 100644 vendor/github.com/xenolf/lego/providers/dns/auroradns/auroradns.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/auroradns/auroradns_test.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/azure/azure.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/azure/azure_test.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/dnspod/dnspod.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/dnspod/dnspod_test.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/exoscale/exoscale.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/exoscale/exoscale_test.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/ns1/ns1.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/ns1/ns1_test.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/rackspace/rackspace.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/rackspace/rackspace_test.go create mode 100644 vendor/github.com/xenolf/lego/providers/http/memcached/README.md create mode 100644 vendor/github.com/xenolf/lego/providers/http/memcached/memcached.go create mode 100644 vendor/github.com/xenolf/lego/providers/http/memcached/memcached_test.go (limited to 'vendor/github.com/xenolf/lego/providers') diff --git a/vendor/github.com/xenolf/lego/providers/dns/auroradns/auroradns.go b/vendor/github.com/xenolf/lego/providers/dns/auroradns/auroradns.go new file mode 100644 index 000000000..55b48f9b4 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/auroradns/auroradns.go @@ -0,0 +1,141 @@ +package auroradns + +import ( + "fmt" + "github.com/edeckers/auroradnsclient" + "github.com/edeckers/auroradnsclient/records" + "github.com/edeckers/auroradnsclient/zones" + "github.com/xenolf/lego/acme" + "os" + "sync" +) + +// DNSProvider describes a provider for AuroraDNS +type DNSProvider struct { + recordIDs map[string]string + recordIDsMu sync.Mutex + client *auroradnsclient.AuroraDNSClient +} + +// NewDNSProvider returns a DNSProvider instance configured for AuroraDNS. +// Credentials must be passed in the environment variables: AURORA_USER_ID +// and AURORA_KEY. +func NewDNSProvider() (*DNSProvider, error) { + userID := os.Getenv("AURORA_USER_ID") + key := os.Getenv("AURORA_KEY") + + endpoint := os.Getenv("AURORA_ENDPOINT") + if endpoint == "" { + endpoint = "https://api.auroradns.eu" + } + + return NewDNSProviderCredentials(endpoint, userID, key) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for AuroraDNS. +func NewDNSProviderCredentials(baseURL string, userID string, key string) (*DNSProvider, error) { + client, err := auroradnsclient.NewAuroraDNSClient(baseURL, userID, key) + if err != nil { + return nil, err + } + + return &DNSProvider{ + client: client, + recordIDs: make(map[string]string), + }, nil +} + +func (provider *DNSProvider) getZoneInformationByName(name string) (zones.ZoneRecord, error) { + zs, err := provider.client.GetZones() + + if err != nil { + return zones.ZoneRecord{}, err + } + + for _, element := range zs { + if element.Name == name { + return element, nil + } + } + + return zones.ZoneRecord{}, fmt.Errorf("Could not find Zone record") +} + +// Present creates a record with a secret +func (provider *DNSProvider) Present(domain, token, keyAuth string) error { + 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) + } + + // 1. Aurora will happily create the TXT record when it is provided a fqdn, + // but it will only appear in the control panel and will not be + // propagated to DNS servers. Extract and use subdomain instead. + // 2. A trailing dot in the fqdn will cause Aurora to add a trailing dot to + // the subdomain, resulting in _acme-challenge.. rather + // than _acme-challenge. + + subdomain := fqdn[0 : len(fqdn)-len(authZone)-1] + + authZone = acme.UnFqdn(authZone) + + zoneRecord, err := provider.getZoneInformationByName(authZone) + + reqData := + records.CreateRecordRequest{ + RecordType: "TXT", + Name: subdomain, + Content: value, + TTL: 300, + } + + respData, err := provider.client.CreateRecord(zoneRecord.ID, reqData) + if err != nil { + return fmt.Errorf("Could not create record: '%s'.", err) + } + + provider.recordIDsMu.Lock() + provider.recordIDs[fqdn] = respData.ID + provider.recordIDsMu.Unlock() + + return nil +} + +// CleanUp removes a given record that was generated by Present +func (provider *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + + provider.recordIDsMu.Lock() + recordID, ok := provider.recordIDs[fqdn] + provider.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("Unknown recordID 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) + + zoneRecord, err := provider.getZoneInformationByName(authZone) + if err != nil { + return err + } + + _, err = provider.client.RemoveRecord(zoneRecord.ID, recordID) + if err != nil { + return err + } + + provider.recordIDsMu.Lock() + delete(provider.recordIDs, fqdn) + provider.recordIDsMu.Unlock() + + return nil +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/auroradns/auroradns_test.go b/vendor/github.com/xenolf/lego/providers/dns/auroradns/auroradns_test.go new file mode 100644 index 000000000..f4df7fa61 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/auroradns/auroradns_test.go @@ -0,0 +1,148 @@ +package auroradns + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +var fakeAuroraDNSUserId = "asdf1234" +var fakeAuroraDNSKey = "key" + +func TestAuroraDNSPresent(t *testing.T) { + var requestReceived bool + + mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" && r.URL.Path == "/zones" { + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `[{ + "id": "c56a4180-65aa-42ec-a945-5fd21dec0538", + "name": "example.com" + }]`) + return + } + + 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, "/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/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) + } + + 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","content":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":300}`; got != want { + + t.Errorf("Expected body data to be: `%s` but got `%s`", want, got) + } + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "id": "c56a4180-65aa-42ec-a945-5fd21dec0538", + "type": "TXT", + "name": "_acme-challenge", + "ttl": 300 + }`) + })) + + defer mock.Close() + + auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserId, fakeAuroraDNSKey) + if auroraProvider == nil { + t.Fatal("Expected non-nil AuroraDNS provider, but was nil") + } + + if err != nil { + t.Fatalf("Expected no error creating provider, but got: %v", err) + } + + err = auroraProvider.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 TestAuroraDNSCleanUp(t *testing.T) { + var requestReceived bool + + mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" && r.URL.Path == "/zones" { + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `[{ + "id": "c56a4180-65aa-42ec-a945-5fd21dec0538", + "name": "example.com" + }]`) + return + } + + if r.Method == "POST" && r.URL.Path == "/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records" { + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "id": "ec56a4180-65aa-42ec-a945-5fd21dec0538", + "type": "TXT", + "name": "_acme-challenge", + "ttl": 300 + }`) + return + } + + 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, + "/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records/ec56a4180-65aa-42ec-a945-5fd21dec0538"; 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) + } + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{}`) + })) + defer mock.Close() + + auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserId, fakeAuroraDNSKey) + if auroraProvider == nil { + t.Fatal("Expected non-nil AuroraDNS provider, but was nil") + } + + if err != nil { + t.Fatalf("Expected no error creating provider, but got: %v", err) + } + + err = auroraProvider.Present("example.com", "", "foobar") + if err != nil { + t.Fatalf("Expected no error creating TXT record, but got: %v", err) + } + + err = auroraProvider.CleanUp("example.com", "", "foobar") + 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/azure/azure.go b/vendor/github.com/xenolf/lego/providers/dns/azure/azure.go new file mode 100644 index 000000000..6742e4f56 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/azure/azure.go @@ -0,0 +1,142 @@ +// Package azure implements a DNS provider for solving the DNS-01 +// challenge using azure DNS. +// Azure doesn't like trailing dots on domain names, most of the acme code does. +package azure + +import ( + "fmt" + "os" + "time" + + "github.com/Azure/azure-sdk-for-go/arm/dns" + + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/to" + "github.com/xenolf/lego/acme" + "strings" +) + +// DNSProvider is an implementation of the acme.ChallengeProvider interface +type DNSProvider struct { + clientId string + clientSecret string + subscriptionId string + tenantId string + resourceGroup string +} + +// NewDNSProvider returns a DNSProvider instance configured for azure. +// Credentials must be passed in the environment variables: AZURE_CLIENT_ID, +// AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID +func NewDNSProvider() (*DNSProvider, error) { + clientId := os.Getenv("AZURE_CLIENT_ID") + clientSecret := os.Getenv("AZURE_CLIENT_SECRET") + subscriptionId := os.Getenv("AZURE_SUBSCRIPTION_ID") + tenantId := os.Getenv("AZURE_TENANT_ID") + resourceGroup := os.Getenv("AZURE_RESOURCE_GROUP") + return NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for azure. +func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup string) (*DNSProvider, error) { + if clientId == "" || clientSecret == "" || subscriptionId == "" || tenantId == "" || resourceGroup == "" { + return nil, fmt.Errorf("Azure configuration missing") + } + + return &DNSProvider{ + clientId: clientId, + clientSecret: clientSecret, + subscriptionId: subscriptionId, + tenantId: tenantId, + resourceGroup: resourceGroup, + }, 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.getHostedZoneID(fqdn) + if err != nil { + return err + } + + rsc := dns.NewRecordSetsClient(c.subscriptionId) + rsc.Authorizer, err = c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint) + relative := toRelativeRecord(fqdn, acme.ToFqdn(zone)) + rec := dns.RecordSet{ + Name: &relative, + Properties: &dns.RecordSetProperties{ + TTL: to.Int64Ptr(60), + TXTRecords: &[]dns.TxtRecord{dns.TxtRecord{Value: &[]string{value}}}, + }, + } + _, err = rsc.CreateOrUpdate(c.resourceGroup, zone, relative, dns.TXT, rec, "", "") + + if err != nil { + return err + } + + return nil +} + +// Returns the relative record to the domain +func toRelativeRecord(domain, zone string) string { + return acme.UnFqdn(strings.TrimSuffix(domain, zone)) +} + +// 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.getHostedZoneID(fqdn) + if err != nil { + return err + } + + relative := toRelativeRecord(fqdn, acme.ToFqdn(zone)) + rsc := dns.NewRecordSetsClient(c.subscriptionId) + rsc.Authorizer, err = c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint) + _, err = rsc.Delete(c.resourceGroup, zone, relative, dns.TXT, "", "") + if err != nil { + return err + } + + return nil +} + +// Checks that azure has a zone for this domain name. +func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) { + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return "", err + } + + // Now we want to to Azure and get the zone. + dc := dns.NewZonesClient(c.subscriptionId) + dc.Authorizer, err = c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint) + zone, err := dc.Get(c.resourceGroup, acme.UnFqdn(authZone)) + + if err != nil { + return "", err + } + + // zone.Name shouldn't have a trailing dot(.) + return to.String(zone.Name), nil +} + +// NewServicePrincipalTokenFromCredentials creates a new ServicePrincipalToken using values of the +// passed credentials map. +func (c *DNSProvider) newServicePrincipalTokenFromCredentials(scope string) (*azure.ServicePrincipalToken, error) { + oauthConfig, err := azure.PublicCloud.OAuthConfigForTenant(c.tenantId) + if err != nil { + panic(err) + } + return azure.NewServicePrincipalToken(*oauthConfig, c.clientId, c.clientSecret, scope) +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/azure/azure_test.go b/vendor/github.com/xenolf/lego/providers/dns/azure/azure_test.go new file mode 100644 index 000000000..db55f578a --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/azure/azure_test.go @@ -0,0 +1,89 @@ +package azure + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + azureLiveTest bool + azureClientID string + azureClientSecret string + azureSubscriptionID string + azureTenantID string + azureResourceGroup string + azureDomain string +) + +func init() { + azureClientID = os.Getenv("AZURE_CLIENT_ID") + azureClientSecret = os.Getenv("AZURE_CLIENT_SECRET") + azureSubscriptionID = os.Getenv("AZURE_SUBSCRIPTION_ID") + azureTenantID = os.Getenv("AZURE_TENANT_ID") + azureResourceGroup = os.Getenv("AZURE_RESOURCE_GROUP") + azureDomain = os.Getenv("AZURE_DOMAIN") + if len(azureClientID) > 0 && len(azureClientSecret) > 0 { + azureLiveTest = true + } +} + +func restoreAzureEnv() { + os.Setenv("AZURE_CLIENT_ID", azureClientID) + os.Setenv("AZURE_SUBSCRIPTION_ID", azureSubscriptionID) +} + +func TestNewDNSProviderValid(t *testing.T) { + if !azureLiveTest { + t.Skip("skipping live test (requires credentials)") + } + os.Setenv("AZURE_CLIENT_ID", "") + _, err := NewDNSProviderCredentials(azureClientID, azureClientSecret, azureSubscriptionID, azureTenantID, azureResourceGroup) + assert.NoError(t, err) + restoreAzureEnv() +} + +func TestNewDNSProviderValidEnv(t *testing.T) { + if !azureLiveTest { + t.Skip("skipping live test (requires credentials)") + } + os.Setenv("AZURE_CLIENT_ID", "other") + _, err := NewDNSProvider() + assert.NoError(t, err) + restoreAzureEnv() +} + +func TestNewDNSProviderMissingCredErr(t *testing.T) { + os.Setenv("AZURE_SUBSCRIPTION_ID", "") + _, err := NewDNSProvider() + assert.EqualError(t, err, "Azure configuration missing") + restoreAzureEnv() +} + +func TestLiveAzurePresent(t *testing.T) { + if !azureLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProviderCredentials(azureClientID, azureClientSecret, azureSubscriptionID, azureTenantID, azureResourceGroup) + assert.NoError(t, err) + + err = provider.Present(azureDomain, "", "123d==") + assert.NoError(t, err) +} + +func TestLiveAzureCleanUp(t *testing.T) { + if !azureLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProviderCredentials(azureClientID, azureClientSecret, azureSubscriptionID, azureTenantID, azureResourceGroup) + time.Sleep(time.Second * 1) + + assert.NoError(t, err) + + err = provider.CleanUp(azureDomain, "", "123d==") + assert.NoError(t, err) +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/dnspod/dnspod.go b/vendor/github.com/xenolf/lego/providers/dns/dnspod/dnspod.go new file mode 100644 index 000000000..0ce08a8bb --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/dnspod/dnspod.go @@ -0,0 +1,146 @@ +// Package dnspod implements a DNS provider for solving the DNS-01 challenge +// using dnspod DNS. +package dnspod + +import ( + "fmt" + "os" + "strings" + + "github.com/decker502/dnspod-go" + "github.com/xenolf/lego/acme" +) + +// DNSProvider is an implementation of the acme.ChallengeProvider interface. +type DNSProvider struct { + client *dnspod.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for dnspod. +// Credentials must be passed in the environment variables: DNSPOD_API_KEY. +func NewDNSProvider() (*DNSProvider, error) { + key := os.Getenv("DNSPOD_API_KEY") + return NewDNSProviderCredentials(key) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for dnspod. +func NewDNSProviderCredentials(key string) (*DNSProvider, error) { + if key == "" { + return nil, fmt.Errorf("dnspod credentials missing") + } + + params := dnspod.CommonParams{LoginToken: key, Format: "json"} + return &DNSProvider{ + client: dnspod.NewClient(params), + }, 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("dnspod 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 + } + + zoneID, _, err := c.getHostedZone(domain) + if err != nil { + return err + } + + for _, rec := range records { + _, err := c.client.Domains.DeleteRecord(zoneID, 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("dnspod API call failed: %v", err) + } + + authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + if err != nil { + return "", "", err + } + + var hostedZone dnspod.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 dnspod for domain %s", authZone, domain) + + } + + return fmt.Sprintf("%v", hostedZone.ID), hostedZone.Name, nil +} + +func (c *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) *dnspod.Record { + name := c.extractRecordName(fqdn, zone) + + return &dnspod.Record{ + Type: "TXT", + Name: name, + Value: value, + Line: "默认", + TTL: "600", + } +} + +func (c *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnspod.Record, error) { + zoneID, zoneName, err := c.getHostedZone(domain) + if err != nil { + return nil, err + } + + var records []dnspod.Record + result, _, err := c.client.Domains.ListRecords(zoneID, "") + if err != nil { + return records, fmt.Errorf("dnspod 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) 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/dnspod/dnspod_test.go b/vendor/github.com/xenolf/lego/providers/dns/dnspod/dnspod_test.go new file mode 100644 index 000000000..3311eb0a6 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/dnspod/dnspod_test.go @@ -0,0 +1,72 @@ +package dnspod + +import ( + "github.com/stretchr/testify/assert" + "os" + "testing" + "time" +) + +var ( + dnspodLiveTest bool + dnspodAPIKey string + dnspodDomain string +) + +func init() { + dnspodAPIKey = os.Getenv("DNSPOD_API_KEY") + dnspodDomain = os.Getenv("DNSPOD_DOMAIN") + if len(dnspodAPIKey) > 0 && len(dnspodDomain) > 0 { + dnspodLiveTest = true + } +} + +func restorednspodEnv() { + os.Setenv("DNSPOD_API_KEY", dnspodAPIKey) +} + +func TestNewDNSProviderValid(t *testing.T) { + os.Setenv("DNSPOD_API_KEY", "") + _, err := NewDNSProviderCredentials("123") + assert.NoError(t, err) + restorednspodEnv() +} +func TestNewDNSProviderValidEnv(t *testing.T) { + os.Setenv("DNSPOD_API_KEY", "123") + _, err := NewDNSProvider() + assert.NoError(t, err) + restorednspodEnv() +} + +func TestNewDNSProviderMissingCredErr(t *testing.T) { + os.Setenv("DNSPOD_API_KEY", "") + _, err := NewDNSProvider() + assert.EqualError(t, err, "dnspod credentials missing") + restorednspodEnv() +} + +func TestLivednspodPresent(t *testing.T) { + if !dnspodLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProviderCredentials(dnspodAPIKey) + assert.NoError(t, err) + + err = provider.Present(dnspodDomain, "", "123d==") + assert.NoError(t, err) +} + +func TestLivednspodCleanUp(t *testing.T) { + if !dnspodLiveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 1) + + provider, err := NewDNSProviderCredentials(dnspodAPIKey) + assert.NoError(t, err) + + err = provider.CleanUp(dnspodDomain, "", "123d==") + assert.NoError(t, err) +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/exoscale/exoscale.go b/vendor/github.com/xenolf/lego/providers/dns/exoscale/exoscale.go new file mode 100644 index 000000000..3b6b58d08 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/exoscale/exoscale.go @@ -0,0 +1,132 @@ +// Package exoscale implements a DNS provider for solving the DNS-01 challenge +// using exoscale DNS. +package exoscale + +import ( + "errors" + "fmt" + "os" + + "github.com/pyr/egoscale/src/egoscale" + "github.com/xenolf/lego/acme" +) + +// DNSProvider is an implementation of the acme.ChallengeProvider interface. +type DNSProvider struct { + client *egoscale.Client +} + +// Credentials must be passed in the environment variables: +// EXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT. +func NewDNSProvider() (*DNSProvider, error) { + key := os.Getenv("EXOSCALE_API_KEY") + secret := os.Getenv("EXOSCALE_API_SECRET") + endpoint := os.Getenv("EXOSCALE_ENDPOINT") + return NewDNSProviderClient(key, secret, endpoint) +} + +// Uses the supplied parameters to return a DNSProvider instance +// configured for Exoscale. +func NewDNSProviderClient(key, secret, endpoint string) (*DNSProvider, error) { + if key == "" || secret == "" { + return nil, fmt.Errorf("Exoscale credentials missing") + } + if endpoint == "" { + endpoint = "https://api.exoscale.ch/dns" + } + + return &DNSProvider{ + client: egoscale.NewClient(endpoint, key, secret), + }, 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, recordName, err := c.FindZoneAndRecordName(fqdn, domain) + if err != nil { + return err + } + + recordId, err := c.FindExistingRecordId(zone, recordName) + if err != nil { + return err + } + + record := egoscale.DNSRecord{ + Name: recordName, + Ttl: ttl, + Content: value, + RecordType: "TXT", + } + + if recordId == 0 { + _, err := c.client.CreateRecord(zone, record) + if err != nil { + return errors.New("Error while creating DNS record: " + err.Error()) + } + } else { + record.Id = recordId + _, err := c.client.UpdateRecord(zone, record) + if err != nil { + return errors.New("Error while updating DNS record: " + err.Error()) + } + } + + return nil +} + +// CleanUp removes the record matching the specified parameters. +func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + zone, recordName, err := c.FindZoneAndRecordName(fqdn, domain) + if err != nil { + return err + } + + recordId, err := c.FindExistingRecordId(zone, recordName) + if err != nil { + return err + } + + if recordId != 0 { + record := egoscale.DNSRecord{ + Id: recordId, + } + + err = c.client.DeleteRecord(zone, record) + if err != nil { + return errors.New("Error while deleting DNS record: " + err.Error()) + } + } + + return nil +} + +// Query Exoscale to find an existing record for this name. +// Returns nil if no record could be found +func (c *DNSProvider) FindExistingRecordId(zone, recordName string) (int64, error) { + responses, err := c.client.GetRecords(zone) + if err != nil { + return -1, errors.New("Error while retrievening DNS records: " + err.Error()) + } + for _, response := range responses { + if response.Record.Name == recordName { + return response.Record.Id, nil + } + } + return 0, nil +} + +// Extract DNS zone and DNS entry name +func (c *DNSProvider) FindZoneAndRecordName(fqdn, domain string) (string, string, error) { + zone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + if err != nil { + return "", "", err + } + zone = acme.UnFqdn(zone) + name := acme.UnFqdn(fqdn) + name = name[:len(name)-len("."+zone)] + + return zone, name, nil +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/exoscale/exoscale_test.go b/vendor/github.com/xenolf/lego/providers/dns/exoscale/exoscale_test.go new file mode 100644 index 000000000..343dd56f8 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/exoscale/exoscale_test.go @@ -0,0 +1,103 @@ +package exoscale + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + exoscaleLiveTest bool + exoscaleAPIKey string + exoscaleAPISecret string + exoscaleDomain string +) + +func init() { + exoscaleAPISecret = os.Getenv("EXOSCALE_API_SECRET") + exoscaleAPIKey = os.Getenv("EXOSCALE_API_KEY") + exoscaleDomain = os.Getenv("EXOSCALE_DOMAIN") + if len(exoscaleAPIKey) > 0 && len(exoscaleAPISecret) > 0 && len(exoscaleDomain) > 0 { + exoscaleLiveTest = true + } +} + +func restoreExoscaleEnv() { + os.Setenv("EXOSCALE_API_KEY", exoscaleAPIKey) + os.Setenv("EXOSCALE_API_SECRET", exoscaleAPISecret) +} + +func TestNewDNSProviderValid(t *testing.T) { + os.Setenv("EXOSCALE_API_KEY", "") + os.Setenv("EXOSCALE_API_SECRET", "") + _, err := NewDNSProviderClient("example@example.com", "123", "") + assert.NoError(t, err) + restoreExoscaleEnv() +} +func TestNewDNSProviderValidEnv(t *testing.T) { + os.Setenv("EXOSCALE_API_KEY", "example@example.com") + os.Setenv("EXOSCALE_API_SECRET", "123") + _, err := NewDNSProvider() + assert.NoError(t, err) + restoreExoscaleEnv() +} + +func TestNewDNSProviderMissingCredErr(t *testing.T) { + os.Setenv("EXOSCALE_API_KEY", "") + os.Setenv("EXOSCALE_API_SECRET", "") + _, err := NewDNSProvider() + assert.EqualError(t, err, "Exoscale credentials missing") + restoreExoscaleEnv() +} + +func TestExtractRootRecordName(t *testing.T) { + provider, err := NewDNSProviderClient("example@example.com", "123", "") + assert.NoError(t, err) + + zone, recordName, err := provider.FindZoneAndRecordName("_acme-challenge.bar.com.", "bar.com") + assert.NoError(t, err) + assert.Equal(t, "bar.com", zone) + assert.Equal(t, "_acme-challenge", recordName) +} + +func TestExtractSubRecordName(t *testing.T) { + provider, err := NewDNSProviderClient("example@example.com", "123", "") + assert.NoError(t, err) + + zone, recordName, err := provider.FindZoneAndRecordName("_acme-challenge.foo.bar.com.", "foo.bar.com") + assert.NoError(t, err) + assert.Equal(t, "bar.com", zone) + assert.Equal(t, "_acme-challenge.foo", recordName) +} + +func TestLiveExoscalePresent(t *testing.T) { + if !exoscaleLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProviderClient(exoscaleAPIKey, exoscaleAPISecret, "") + assert.NoError(t, err) + + err = provider.Present(exoscaleDomain, "", "123d==") + assert.NoError(t, err) + + // Present Twice to handle create / update + err = provider.Present(exoscaleDomain, "", "123d==") + assert.NoError(t, err) +} + +func TestLiveExoscaleCleanUp(t *testing.T) { + if !exoscaleLiveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 1) + + provider, err := NewDNSProviderClient(exoscaleAPIKey, exoscaleAPISecret, "") + assert.NoError(t, err) + + err = provider.CleanUp(exoscaleDomain, "", "123d==") + assert.NoError(t, err) +} 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 index 15919e2eb..451333ca1 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi_test.go +++ b/vendor/github.com/xenolf/lego/providers/dns/gandi/gandi_test.go @@ -141,7 +141,7 @@ func TestDNSProviderLive(t *testing.T) { } // complete the challenge bundle := false - _, failures := client.ObtainCertificate([]string{domain}, bundle, nil) + _, failures := client.ObtainCertificate([]string{domain}, bundle, nil, false) if len(failures) > 0 { t.Fatal(failures) } @@ -496,7 +496,7 @@ var serverResponses = map[string]string{ id -3333333333 +333333333 value diff --git a/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go b/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go index b8d9951c9..ea6c0875c 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go +++ b/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud.go @@ -68,6 +68,16 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error { Additions: []*dns.ResourceRecordSet{rec}, } + // Look for existing records. + list, err := c.client.ResourceRecordSets.List(c.project, zone).Name(fqdn).Type("TXT").Do() + if err != nil { + return err + } + if len(list.Rrsets) > 0 { + // Attempt to delete the existing records when adding our new one. + change.Deletions = list.Rrsets + } + chg, err := c.client.Changes.Create(c.project, zone, change).Do() if err != nil { return err 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 index d73788163..75a10d9a4 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud_test.go +++ b/vendor/github.com/xenolf/lego/providers/dns/googlecloud/googlecloud_test.go @@ -70,6 +70,20 @@ func TestLiveGoogleCloudPresent(t *testing.T) { assert.NoError(t, err) } +func TestLiveGoogleCloudPresentMultiple(t *testing.T) { + if !gcloudLiveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProviderCredentials(gcloudProject) + assert.NoError(t, err) + + // Check that we're able to create multiple entries + err = provider.Present(gcloudDomain, "1", "123d==") + err = provider.Present(gcloudDomain, "2", "123d==") + assert.NoError(t, err) +} + func TestLiveGoogleCloudCleanUp(t *testing.T) { if !gcloudLiveTest { t.Skip("skipping live test") diff --git a/vendor/github.com/xenolf/lego/providers/dns/ns1/ns1.go b/vendor/github.com/xenolf/lego/providers/dns/ns1/ns1.go new file mode 100644 index 000000000..105d73f89 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/ns1/ns1.go @@ -0,0 +1,97 @@ +// Package ns1 implements a DNS provider for solving the DNS-01 challenge +// using NS1 DNS. +package ns1 + +import ( + "fmt" + "net/http" + "os" + "time" + + "github.com/xenolf/lego/acme" + "gopkg.in/ns1/ns1-go.v2/rest" + "gopkg.in/ns1/ns1-go.v2/rest/model/dns" +) + +// DNSProvider is an implementation of the acme.ChallengeProvider interface. +type DNSProvider struct { + client *rest.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for NS1. +// Credentials must be passed in the environment variables: NS1_API_KEY. +func NewDNSProvider() (*DNSProvider, error) { + key := os.Getenv("NS1_API_KEY") + if key == "" { + return nil, fmt.Errorf("NS1 credentials missing") + } + return NewDNSProviderCredentials(key) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for NS1. +func NewDNSProviderCredentials(key string) (*DNSProvider, error) { + if key == "" { + return nil, fmt.Errorf("NS1 credentials missing") + } + + httpClient := &http.Client{Timeout: time.Second * 10} + client := rest.NewClient(httpClient, rest.SetAPIKey(key)) + + return &DNSProvider{client}, nil +} + +// Present creates a TXT record to fulfil the dns-01 challenge. +func (c *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) + + zone, err := c.getHostedZone(domain) + if err != nil { + return err + } + + record := c.newTxtRecord(zone, fqdn, value, ttl) + _, err = c.client.Records.Create(record) + if err != nil && err != rest.ErrRecordExists { + return err + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + + zone, err := c.getHostedZone(domain) + if err != nil { + return err + } + + name := acme.UnFqdn(fqdn) + _, err = c.client.Records.Delete(zone.Zone, name, "TXT") + return err +} + +func (c *DNSProvider) getHostedZone(domain string) (*dns.Zone, error) { + zone, _, err := c.client.Zones.Get(domain) + if err != nil { + return nil, err + } + + return zone, nil +} + +func (c *DNSProvider) newTxtRecord(zone *dns.Zone, fqdn, value string, ttl int) *dns.Record { + name := acme.UnFqdn(fqdn) + + return &dns.Record{ + Type: "TXT", + Zone: zone.Zone, + Domain: name, + TTL: ttl, + Answers: []*dns.Answer{ + {Rdata: []string{value}}, + }, + } +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/ns1/ns1_test.go b/vendor/github.com/xenolf/lego/providers/dns/ns1/ns1_test.go new file mode 100644 index 000000000..eb9150dde --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/ns1/ns1_test.go @@ -0,0 +1,67 @@ +package ns1 + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + liveTest bool + apiKey string + domain string +) + +func init() { + apiKey = os.Getenv("NS1_API_KEY") + domain = os.Getenv("NS1_DOMAIN") + if len(apiKey) > 0 && len(domain) > 0 { + liveTest = true + } +} + +func restoreNS1Env() { + os.Setenv("NS1_API_KEY", apiKey) +} + +func TestNewDNSProviderValid(t *testing.T) { + os.Setenv("NS1_API_KEY", "") + _, err := NewDNSProviderCredentials("123") + assert.NoError(t, err) + restoreNS1Env() +} + +func TestNewDNSProviderMissingCredErr(t *testing.T) { + os.Setenv("NS1_API_KEY", "") + _, err := NewDNSProvider() + assert.EqualError(t, err, "NS1 credentials missing") + restoreNS1Env() +} + +func TestLivePresent(t *testing.T) { + if !liveTest { + t.Skip("skipping live test") + } + + provider, err := NewDNSProviderCredentials(apiKey) + assert.NoError(t, err) + + err = provider.Present(domain, "", "123d==") + assert.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !liveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 1) + + provider, err := NewDNSProviderCredentials(apiKey) + assert.NoError(t, err) + + err = provider.CleanUp(domain, "", "123d==") + assert.NoError(t, err) +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/rackspace/rackspace.go b/vendor/github.com/xenolf/lego/providers/dns/rackspace/rackspace.go new file mode 100644 index 000000000..2b106a27e --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/rackspace/rackspace.go @@ -0,0 +1,284 @@ +// Package rackspace implements a DNS provider for solving the DNS-01 +// challenge using rackspace DNS. +package rackspace + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/xenolf/lego/acme" +) + +// rackspaceAPIURL represents the Identity API endpoint to call +var rackspaceAPIURL = "https://identity.api.rackspacecloud.com/v2.0/tokens" + +// DNSProvider is an implementation of the acme.ChallengeProvider interface +// used to store the reusable token and DNS API endpoint +type DNSProvider struct { + token string + cloudDNSEndpoint string +} + +// NewDNSProvider returns a DNSProvider instance configured for Rackspace. +// Credentials must be passed in the environment variables: RACKSPACE_USER +// and RACKSPACE_API_KEY. +func NewDNSProvider() (*DNSProvider, error) { + user := os.Getenv("RACKSPACE_USER") + key := os.Getenv("RACKSPACE_API_KEY") + return NewDNSProviderCredentials(user, key) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for Rackspace. It authenticates against +// the API, also grabbing the DNS Endpoint. +func NewDNSProviderCredentials(user, key string) (*DNSProvider, error) { + if user == "" || key == "" { + return nil, fmt.Errorf("Rackspace credentials missing") + } + + type APIKeyCredentials struct { + Username string `json:"username"` + APIKey string `json:"apiKey"` + } + + type Auth struct { + APIKeyCredentials `json:"RAX-KSKEY:apiKeyCredentials"` + } + + type RackspaceAuthData struct { + Auth `json:"auth"` + } + + type RackspaceIdentity struct { + Access struct { + ServiceCatalog []struct { + Endpoints []struct { + PublicURL string `json:"publicURL"` + TenantID string `json:"tenantId"` + } `json:"endpoints"` + Name string `json:"name"` + } `json:"serviceCatalog"` + Token struct { + ID string `json:"id"` + } `json:"token"` + } `json:"access"` + } + + authData := RackspaceAuthData{ + Auth: Auth{ + APIKeyCredentials: APIKeyCredentials{ + Username: user, + APIKey: key, + }, + }, + } + + body, err := json.Marshal(authData) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", rackspaceAPIURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + client := http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("Error querying Rackspace Identity API: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Rackspace Authentication failed. Response code: %d", resp.StatusCode) + } + + var rackspaceIdentity RackspaceIdentity + err = json.NewDecoder(resp.Body).Decode(&rackspaceIdentity) + if err != nil { + return nil, err + } + + // Iterate through the Service Catalog to get the DNS Endpoint + var dnsEndpoint string + for _, service := range rackspaceIdentity.Access.ServiceCatalog { + if service.Name == "cloudDNS" { + dnsEndpoint = service.Endpoints[0].PublicURL + break + } + } + if dnsEndpoint == "" { + return nil, fmt.Errorf("Failed to populate DNS endpoint, check Rackspace API for changes.") + } + + return &DNSProvider{ + token: rackspaceIdentity.Access.Token.ID, + cloudDNSEndpoint: dnsEndpoint, + }, nil +} + +// 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 := RackspaceRecords{ + RackspaceRecord: []RackspaceRecord{{ + Name: acme.UnFqdn(fqdn), + Type: "TXT", + Data: value, + TTL: 300, + }}, + } + + body, err := json.Marshal(rec) + if err != nil { + return err + } + + _, err = c.makeRequest("POST", fmt.Sprintf("/domains/%d/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) + zoneID, err := c.getHostedZoneID(fqdn) + if err != nil { + return err + } + + record, err := c.findTxtRecord(fqdn, zoneID) + if err != nil { + return err + } + + _, err = c.makeRequest("DELETE", fmt.Sprintf("/domains/%d/records?id=%s", zoneID, record.ID), nil) + if err != nil { + return err + } + + return nil +} + +// getHostedZoneID performs a lookup to get the DNS zone which needs +// modifying for a given FQDN +func (c *DNSProvider) getHostedZoneID(fqdn string) (int, error) { + // HostedZones represents the response when querying Rackspace DNS zones + type ZoneSearchResponse struct { + TotalEntries int `json:"totalEntries"` + HostedZones []struct { + ID int `json:"id"` + Name string `json:"name"` + } `json:"domains"` + } + + authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + if err != nil { + return 0, err + } + + result, err := c.makeRequest("GET", fmt.Sprintf("/domains?name=%s", acme.UnFqdn(authZone)), nil) + if err != nil { + return 0, err + } + + var zoneSearchResponse ZoneSearchResponse + err = json.Unmarshal(result, &zoneSearchResponse) + if err != nil { + return 0, err + } + + // If nothing was returned, or for whatever reason more than 1 was returned (the search uses exact match, so should not occur) + if zoneSearchResponse.TotalEntries != 1 { + return 0, fmt.Errorf("Found %d zones for %s in Rackspace for domain %s", zoneSearchResponse.TotalEntries, authZone, fqdn) + } + + return zoneSearchResponse.HostedZones[0].ID, nil +} + +// findTxtRecord searches a DNS zone for a TXT record with a specific name +func (c *DNSProvider) findTxtRecord(fqdn string, zoneID int) (*RackspaceRecord, error) { + result, err := c.makeRequest("GET", fmt.Sprintf("/domains/%d/records?type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)), nil) + if err != nil { + return nil, err + } + + var records RackspaceRecords + err = json.Unmarshal(result, &records) + if err != nil { + return nil, err + } + + recordsLength := len(records.RackspaceRecord) + switch recordsLength { + case 1: + break + case 0: + return nil, fmt.Errorf("No TXT record found for %s", fqdn) + default: + return nil, fmt.Errorf("More than 1 TXT record found for %s", fqdn) + } + + return &records.RackspaceRecord[0], nil +} + +// makeRequest is a wrapper function used for making DNS API requests +func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) { + url := c.cloudDNSEndpoint + uri + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + req.Header.Set("X-Auth-Token", c.token) + req.Header.Set("Content-Type", "application/json") + + client := http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("Error querying DNS API: %v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { + return nil, fmt.Errorf("Request failed for %s %s. Response code: %d", method, url, resp.StatusCode) + } + + var r json.RawMessage + err = json.NewDecoder(resp.Body).Decode(&r) + if err != nil { + return nil, fmt.Errorf("JSON decode failed for %s %s. Response code: %d", method, url, resp.StatusCode) + } + + return r, nil +} + +// RackspaceRecords is the list of records sent/recieved from the DNS API +type RackspaceRecords struct { + RackspaceRecord []RackspaceRecord `json:"records"` +} + +// RackspaceRecord represents a Rackspace DNS record +type RackspaceRecord struct { + Name string `json:"name"` + Type string `json:"type"` + Data string `json:"data"` + TTL int `json:"ttl,omitempty"` + ID string `json:"id,omitempty"` +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/rackspace/rackspace_test.go b/vendor/github.com/xenolf/lego/providers/dns/rackspace/rackspace_test.go new file mode 100644 index 000000000..22c979cad --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/rackspace/rackspace_test.go @@ -0,0 +1,220 @@ +package rackspace + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var ( + rackspaceLiveTest bool + rackspaceUser string + rackspaceAPIKey string + rackspaceDomain string + testAPIURL string +) + +func init() { + rackspaceUser = os.Getenv("RACKSPACE_USER") + rackspaceAPIKey = os.Getenv("RACKSPACE_API_KEY") + rackspaceDomain = os.Getenv("RACKSPACE_DOMAIN") + if len(rackspaceUser) > 0 && len(rackspaceAPIKey) > 0 && len(rackspaceDomain) > 0 { + rackspaceLiveTest = true + } +} + +func testRackspaceEnv() { + rackspaceAPIURL = testAPIURL + os.Setenv("RACKSPACE_USER", "testUser") + os.Setenv("RACKSPACE_API_KEY", "testKey") +} + +func liveRackspaceEnv() { + rackspaceAPIURL = "https://identity.api.rackspacecloud.com/v2.0/tokens" + os.Setenv("RACKSPACE_USER", rackspaceUser) + os.Setenv("RACKSPACE_API_KEY", rackspaceAPIKey) +} + +func startTestServers() (identityAPI, dnsAPI *httptest.Server) { + dnsAPI = httptest.NewServer(dnsMux()) + dnsEndpoint := dnsAPI.URL + "/123456" + + identityAPI = httptest.NewServer(identityHandler(dnsEndpoint)) + testAPIURL = identityAPI.URL + "/" + return +} + +func closeTestServers(identityAPI, dnsAPI *httptest.Server) { + identityAPI.Close() + dnsAPI.Close() +} + +func identityHandler(dnsEndpoint string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqBody, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + resp, found := jsonMap[string(reqBody)] + if !found { + w.WriteHeader(http.StatusBadRequest) + return + } + resp = strings.Replace(resp, "https://dns.api.rackspacecloud.com/v1.0/123456", dnsEndpoint, 1) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, resp) + }) +} + +func dnsMux() *http.ServeMux { + mux := http.NewServeMux() + + // Used by `getHostedZoneID()` finding `zoneID` "?name=example.com" + mux.HandleFunc("/123456/domains", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("name") == "example.com" { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["zoneDetails"]) + return + } + w.WriteHeader(http.StatusBadRequest) + return + }) + + mux.HandleFunc("/123456/domains/112233/records", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + // Used by `Present()` creating the TXT record + case http.MethodPost: + reqBody, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + resp, found := jsonMap[string(reqBody)] + if !found { + w.WriteHeader(http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusAccepted) + fmt.Fprintf(w, resp) + // Used by `findTxtRecord()` finding `record.ID` "?type=TXT&name=_acme-challenge.example.com" + case http.MethodGet: + if r.URL.Query().Get("type") == "TXT" && r.URL.Query().Get("name") == "_acme-challenge.example.com" { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["recordDetails"]) + return + } + w.WriteHeader(http.StatusBadRequest) + return + // Used by `CleanUp()` deleting the TXT record "?id=445566" + case http.MethodDelete: + if r.URL.Query().Get("id") == "TXT-654321" { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["recordDelete"]) + return + } + w.WriteHeader(http.StatusBadRequest) + } + }) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Printf("Not Found for Request: (%+v)\n\n", r) + }) + + return mux +} + +func TestNewDNSProviderMissingCredErr(t *testing.T) { + testRackspaceEnv() + _, err := NewDNSProviderCredentials("", "") + assert.EqualError(t, err, "Rackspace credentials missing") +} + +func TestOfflineRackspaceValid(t *testing.T) { + testRackspaceEnv() + provider, err := NewDNSProviderCredentials(os.Getenv("RACKSPACE_USER"), os.Getenv("RACKSPACE_API_KEY")) + + assert.NoError(t, err) + assert.Equal(t, provider.token, "testToken", "The token should match") +} + +func TestOfflineRackspacePresent(t *testing.T) { + testRackspaceEnv() + provider, err := NewDNSProvider() + + if assert.NoError(t, err) { + err = provider.Present("example.com", "token", "keyAuth") + assert.NoError(t, err) + } +} + +func TestOfflineRackspaceCleanUp(t *testing.T) { + testRackspaceEnv() + provider, err := NewDNSProvider() + + if assert.NoError(t, err) { + err = provider.CleanUp("example.com", "token", "keyAuth") + assert.NoError(t, err) + } +} + +func TestNewDNSProviderValidEnv(t *testing.T) { + if !rackspaceLiveTest { + t.Skip("skipping live test") + } + + liveRackspaceEnv() + provider, err := NewDNSProvider() + assert.NoError(t, err) + assert.Contains(t, provider.cloudDNSEndpoint, "https://dns.api.rackspacecloud.com/v1.0/", "The endpoint URL should contain the base") +} + +func TestRackspacePresent(t *testing.T) { + if !rackspaceLiveTest { + t.Skip("skipping live test") + } + + liveRackspaceEnv() + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.Present(rackspaceDomain, "", "112233445566==") + assert.NoError(t, err) +} + +func TestRackspaceCleanUp(t *testing.T) { + if !rackspaceLiveTest { + t.Skip("skipping live test") + } + + time.Sleep(time.Second * 15) + + liveRackspaceEnv() + provider, err := NewDNSProvider() + assert.NoError(t, err) + + err = provider.CleanUp(rackspaceDomain, "", "112233445566==") + assert.NoError(t, err) +} + +func TestMain(m *testing.M) { + identityAPI, dnsAPI := startTestServers() + defer closeTestServers(identityAPI, dnsAPI) + os.Exit(m.Run()) +} + +var jsonMap = map[string]string{ + `{"auth":{"RAX-KSKEY:apiKeyCredentials":{"username":"testUser","apiKey":"testKey"}}}`: `{"access":{"token":{"id":"testToken","expires":"1970-01-01T00:00:00.000Z","tenant":{"id":"123456","name":"123456"},"RAX-AUTH:authenticatedBy":["APIKEY"]},"serviceCatalog":[{"type":"rax:dns","endpoints":[{"publicURL":"https://dns.api.rackspacecloud.com/v1.0/123456","tenantId":"123456"}],"name":"cloudDNS"}],"user":{"id":"fakeUseID","name":"testUser"}}}`, + "zoneDetails": `{"domains":[{"name":"example.com","id":112233,"emailAddress":"hostmaster@example.com","updated":"1970-01-01T00:00:00.000+0000","created":"1970-01-01T00:00:00.000+0000"}],"totalEntries":1}`, + `{"records":[{"name":"_acme-challenge.example.com","type":"TXT","data":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM","ttl":300}]}`: `{"request":"{\"records\":[{\"name\":\"_acme-challenge.example.com\",\"type\":\"TXT\",\"data\":\"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\"ttl\":300}]}","status":"RUNNING","verb":"POST","jobId":"00000000-0000-0000-0000-0000000000","callbackUrl":"https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000","requestUrl":"https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/records"}`, + "recordDetails": `{"records":[{"name":"_acme-challenge.example.com","id":"TXT-654321","type":"TXT","data":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM","ttl":300,"updated":"1970-01-01T00:00:00.000+0000","created":"1970-01-01T00:00:00.000+0000"}]}`, + "recordDelete": `{"status":"RUNNING","verb":"DELETE","jobId":"00000000-0000-0000-0000-0000000000","callbackUrl":"https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000","requestUrl":"https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/recordsid=TXT-654321"}`, +} diff --git a/vendor/github.com/xenolf/lego/providers/http/memcached/README.md b/vendor/github.com/xenolf/lego/providers/http/memcached/README.md new file mode 100644 index 000000000..f14d216df --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/http/memcached/README.md @@ -0,0 +1,15 @@ +# Memcached http provider + +Publishes challenges into memcached where they can be retrieved by nginx. Allows +specifying multiple memcached servers and the responses will be published to all +of them, making it easier to verify when your domain is hosted on a cluster of +servers. + +Example nginx config: + +``` + location /.well-known/acme-challenge/ { + set $memcached_key "$uri"; + memcached_pass 127.0.0.1:11211; + } +``` diff --git a/vendor/github.com/xenolf/lego/providers/http/memcached/memcached.go b/vendor/github.com/xenolf/lego/providers/http/memcached/memcached.go new file mode 100644 index 000000000..9c5f6c0b4 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/http/memcached/memcached.go @@ -0,0 +1,59 @@ +// Package webroot implements a HTTP provider for solving the HTTP-01 challenge using web server's root path. +package memcached + +import ( + "fmt" + "path" + + "github.com/rainycape/memcache" + "github.com/xenolf/lego/acme" +) + +// HTTPProvider implements ChallengeProvider for `http-01` challenge +type MemcachedProvider struct { + hosts []string +} + +// NewHTTPProvider returns a HTTPProvider instance with a configured webroot path +func NewMemcachedProvider(hosts []string) (*MemcachedProvider, error) { + if len(hosts) == 0 { + return nil, fmt.Errorf("No memcached hosts provided") + } + + c := &MemcachedProvider{ + hosts: hosts, + } + + return c, nil +} + +// Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given webroot path +func (w *MemcachedProvider) Present(domain, token, keyAuth string) error { + var errs []error + + challengePath := path.Join("/", acme.HTTP01ChallengePath(token)) + for _, host := range w.hosts { + mc, err := memcache.New(host) + if err != nil { + errs = append(errs, err) + continue + } + mc.Add(&memcache.Item{ + Key: challengePath, + Value: []byte(keyAuth), + Expiration: 60, + }) + } + + if len(errs) == len(w.hosts) { + return fmt.Errorf("Unable to store key in any of the memcache hosts -> %v", errs) + } + + return nil +} + +// CleanUp removes the file created for the challenge +func (w *MemcachedProvider) CleanUp(domain, token, keyAuth string) error { + // Memcached will clean up itself, that's what expiration is for. + return nil +} diff --git a/vendor/github.com/xenolf/lego/providers/http/memcached/memcached_test.go b/vendor/github.com/xenolf/lego/providers/http/memcached/memcached_test.go new file mode 100644 index 000000000..287a33304 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/http/memcached/memcached_test.go @@ -0,0 +1,111 @@ +package memcached + +import ( + "os" + "path" + "strings" + "testing" + + "github.com/rainycape/memcache" + "github.com/stretchr/testify/assert" + "github.com/xenolf/lego/acme" +) + +var ( + memcachedHosts []string +) + +const ( + domain = "lego.test" + token = "foo" + keyAuth = "bar" +) + +func init() { + memcachedHostsStr := os.Getenv("MEMCACHED_HOSTS") + if len(memcachedHostsStr) > 0 { + memcachedHosts = strings.Split(memcachedHostsStr, ",") + } +} + +func TestNewMemcachedProviderEmpty(t *testing.T) { + emptyHosts := make([]string, 0) + _, err := NewMemcachedProvider(emptyHosts) + assert.EqualError(t, err, "No memcached hosts provided") +} + +func TestNewMemcachedProviderValid(t *testing.T) { + if len(memcachedHosts) == 0 { + t.Skip("Skipping memcached tests") + } + _, err := NewMemcachedProvider(memcachedHosts) + assert.NoError(t, err) +} + +func TestMemcachedPresentSingleHost(t *testing.T) { + if len(memcachedHosts) == 0 { + t.Skip("Skipping memcached tests") + } + p, err := NewMemcachedProvider(memcachedHosts[0:1]) + assert.NoError(t, err) + + challengePath := path.Join("/", acme.HTTP01ChallengePath(token)) + + err = p.Present(domain, token, keyAuth) + assert.NoError(t, err) + mc, err := memcache.New(memcachedHosts[0]) + assert.NoError(t, err) + i, err := mc.Get(challengePath) + assert.NoError(t, err) + assert.Equal(t, i.Value, []byte(keyAuth)) +} + +func TestMemcachedPresentMultiHost(t *testing.T) { + if len(memcachedHosts) <= 1 { + t.Skip("Skipping memcached multi-host tests") + } + p, err := NewMemcachedProvider(memcachedHosts) + assert.NoError(t, err) + + challengePath := path.Join("/", acme.HTTP01ChallengePath(token)) + + err = p.Present(domain, token, keyAuth) + assert.NoError(t, err) + for _, host := range memcachedHosts { + mc, err := memcache.New(host) + assert.NoError(t, err) + i, err := mc.Get(challengePath) + assert.NoError(t, err) + assert.Equal(t, i.Value, []byte(keyAuth)) + } +} + +func TestMemcachedPresentPartialFailureMultiHost(t *testing.T) { + if len(memcachedHosts) == 0 { + t.Skip("Skipping memcached tests") + } + hosts := append(memcachedHosts, "5.5.5.5:11211") + p, err := NewMemcachedProvider(hosts) + assert.NoError(t, err) + + challengePath := path.Join("/", acme.HTTP01ChallengePath(token)) + + err = p.Present(domain, token, keyAuth) + assert.NoError(t, err) + for _, host := range memcachedHosts { + mc, err := memcache.New(host) + assert.NoError(t, err) + i, err := mc.Get(challengePath) + assert.NoError(t, err) + assert.Equal(t, i.Value, []byte(keyAuth)) + } +} + +func TestMemcachedCleanup(t *testing.T) { + if len(memcachedHosts) == 0 { + t.Skip("Skipping memcached tests") + } + p, err := NewMemcachedProvider(memcachedHosts) + assert.NoError(t, err) + assert.NoError(t, p.CleanUp(domain, token, keyAuth)) +} -- cgit v1.2.3-1-g7c22