lego/acme/dns_challenge_route53.go
Matthew Holt 971541dc0a Use http client with timeout of 10s
This will prevent indefinitely-hanging requests in case some service or middle box is malfunctioning.

Fix vet errors and lint warnings

Add vet to CI check

Only get issuer certificate if it would be used

No need to make a GET request if the OCSP server is not specified in leaf certificate

Fix CI tests

Make tests verbose
2016-02-14 14:33:54 -07:00

137 lines
4.1 KiB
Go

package acme
import (
"fmt"
"strings"
"time"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/route53"
)
// DNSProviderRoute53 is an implementation of the DNSProvider interface
type DNSProviderRoute53 struct {
client *route53.Route53
}
// NewDNSProviderRoute53 returns a DNSProviderRoute53 instance with a configured route53 client.
// Authentication is either done using the passed credentials or - when empty - falling back to
// the customary AWS credential mechanisms, including the file refernced by $AWS_CREDENTIAL_FILE
// (defaulting to $HOME/.aws/credentials) optionally scoped to $AWS_PROFILE, credentials
// supplied by the environment variables AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY [ + AWS_SECURITY_TOKEN ],
// and finally credentials available via the EC2 instance metadata service.
func NewDNSProviderRoute53(awsAccessKey, awsSecretKey, awsRegionName string) (*DNSProviderRoute53, error) {
region, ok := aws.Regions[awsRegionName]
if !ok {
return nil, fmt.Errorf("Invalid AWS region name %s", awsRegionName)
}
// use aws.GetAuth, which tries really hard to find credentails:
// - uses awsAccessKey and awsSecretKey, if provided
// - uses AWS_PROFILE / AWS_CREDENTIAL_FILE, if provided
// - uses AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY and optionally AWS_SECURITY_TOKEN, if provided
// - uses EC2 instance metadata credentials (http://169.254.169.254/latest/meta-data/…), if available
// ...and otherwise returns an error
auth, err := aws.GetAuth(awsAccessKey, awsSecretKey)
if err != nil {
return nil, err
}
client := route53.New(auth, region)
return &DNSProviderRoute53{client: client}, nil
}
// Present creates a TXT record using the specified parameters
func (r *DNSProviderRoute53) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := DNS01Record(domain, keyAuth)
value = `"` + value + `"`
return r.changeRecord("UPSERT", fqdn, value, ttl)
}
// CleanUp removes the TXT record matching the specified parameters
func (r *DNSProviderRoute53) CleanUp(domain, token, keyAuth string) error {
fqdn, value, ttl := DNS01Record(domain, keyAuth)
value = `"` + value + `"`
return r.changeRecord("DELETE", fqdn, value, ttl)
}
func (r *DNSProviderRoute53) changeRecord(action, fqdn, value string, ttl int) error {
hostedZoneID, err := r.getHostedZoneID(fqdn)
if err != nil {
return err
}
recordSet := newTXTRecordSet(fqdn, value, ttl)
update := route53.Change{Action: action, Record: recordSet}
changes := []route53.Change{update}
req := route53.ChangeResourceRecordSetsRequest{Comment: "Created by Lego", Changes: changes}
resp, err := r.client.ChangeResourceRecordSets(hostedZoneID, &req)
if err != nil {
return err
}
return waitFor(90, 5, func() (bool, error) {
status, err := r.client.GetChange(resp.ChangeInfo.ID)
if err != nil {
return false, err
}
if status == "INSYNC" {
return true, nil
}
return false, nil
})
}
func (r *DNSProviderRoute53) getHostedZoneID(fqdn string) (string, error) {
zones := []route53.HostedZone{}
zoneResp, err := r.client.ListHostedZones("", 0)
if err != nil {
return "", err
}
zones = append(zones, zoneResp.HostedZones...)
for zoneResp.IsTruncated {
resp, err := r.client.ListHostedZones(zoneResp.Marker, 0)
if err != nil {
if rateExceeded(err) {
time.Sleep(time.Second)
continue
}
return "", err
}
zoneResp = resp
zones = append(zones, zoneResp.HostedZones...)
}
var hostedZone route53.HostedZone
for _, zone := range zones {
if strings.HasSuffix(fqdn, zone.Name) {
if len(zone.Name) > len(hostedZone.Name) {
hostedZone = zone
}
}
}
if hostedZone.ID == "" {
return "", fmt.Errorf("No Route53 hosted zone found for domain %s", fqdn)
}
return hostedZone.ID, nil
}
func newTXTRecordSet(fqdn, value string, ttl int) route53.ResourceRecordSet {
return route53.ResourceRecordSet{
Name: fqdn,
Type: "TXT",
Records: []string{value},
TTL: ttl,
}
}
// Route53 API has pretty strict rate limits (5req/s globally per account)
// Hence we check if we are being throttled to maybe retry the request
func rateExceeded(err error) bool {
if strings.Contains(err.Error(), "Throttling") {
return true
}
return false
}