2016-08-29 19:37:19 +00:00
|
|
|
// Package linode implements a DNS provider for solving the DNS-01 challenge
|
|
|
|
// using Linode DNS.
|
|
|
|
package linode
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2018-06-11 15:32:50 +00:00
|
|
|
"fmt"
|
2016-08-29 19:37:19 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/timewasted/linode/dns"
|
|
|
|
"github.com/xenolf/lego/acme"
|
2018-06-11 15:32:50 +00:00
|
|
|
"github.com/xenolf/lego/platform/config/env"
|
2016-08-29 19:37:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
dnsMinTTLSecs = 300
|
|
|
|
dnsUpdateFreqMins = 15
|
|
|
|
dnsUpdateFudgeSecs = 120
|
|
|
|
)
|
|
|
|
|
|
|
|
type hostedZoneInfo struct {
|
2018-05-30 17:53:04 +00:00
|
|
|
domainID int
|
2016-08-29 19:37:19 +00:00
|
|
|
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) {
|
2018-06-11 15:32:50 +00:00
|
|
|
values, err := env.Get("LINODE_API_KEY")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Linode: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return NewDNSProviderCredentials(values["LINODE_API_KEY"])
|
2016-08-29 19:37:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2018-05-30 17:53:04 +00:00
|
|
|
if _, err = p.linode.CreateDomainResourceTXT(zone.domainID, acme.UnFqdn(fqdn), value, 60); err != nil {
|
2016-08-29 19:37:19 +00:00
|
|
|
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.
|
2018-05-30 17:53:04 +00:00
|
|
|
resources, err := p.linode.GetResourcesByType(zone.domainID, "TXT")
|
2016-08-29 19:37:19 +00:00
|
|
|
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 {
|
2018-05-30 17:53:04 +00:00
|
|
|
return errors.New("error deleting resource: resource IDs do not match")
|
2016-08-29 19:37:19 +00:00
|
|
|
}
|
|
|
|
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{
|
2018-05-30 17:53:04 +00:00
|
|
|
domainID: domain.DomainID,
|
2016-08-29 19:37:19 +00:00
|
|
|
resourceName: resourceName,
|
|
|
|
}, nil
|
|
|
|
}
|