971541dc0a
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
137 lines
4.1 KiB
Go
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
|
|
}
|