summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/xenolf/lego/providers/dns/linode
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/xenolf/lego/providers/dns/linode')
-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
2 files changed, 448 insertions, 0 deletions
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")
+ }
+}