diff options
author | Christopher Speller <crspeller@gmail.com> | 2016-10-03 16:03:15 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-10-03 16:03:15 -0400 |
commit | 8f91c777559748fa6e857d9fc1f4ae079a532813 (patch) | |
tree | 190f7cef373764a0d47a91045fdb486ee3d6781d /vendor/github.com/xenolf/lego/providers/dns/route53 | |
parent | 5f8e5c401bd96cba9a98b2db02d72f9cbacb0103 (diff) | |
download | chat-8f91c777559748fa6e857d9fc1f4ae079a532813.tar.gz chat-8f91c777559748fa6e857d9fc1f4ae079a532813.tar.bz2 chat-8f91c777559748fa6e857d9fc1f4ae079a532813.zip |
Adding ability to serve TLS directly from Mattermost server (#4119)
Diffstat (limited to 'vendor/github.com/xenolf/lego/providers/dns/route53')
5 files changed, 405 insertions, 0 deletions
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 +} |