2020-07-08 20:16:54 +00:00
// Package edgedns replaces fastdns, implementing a DNS provider for solving the DNS-01 challenge using Akamai EdgeDNS.
package edgedns
import (
"errors"
"fmt"
"strings"
"time"
configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
2020-09-02 01:20:01 +00:00
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/config/env"
2020-07-08 20:16:54 +00:00
)
// Environment variables names.
const (
envNamespace = "AKAMAI_"
2021-02-21 10:56:56 +00:00
EnvEdgeRc = envNamespace + "EDGERC"
EnvEdgeRcSection = envNamespace + "EDGERC_SECTION"
2020-07-08 20:16:54 +00:00
EnvHost = envNamespace + "HOST"
EnvClientToken = envNamespace + "CLIENT_TOKEN"
EnvClientSecret = envNamespace + "CLIENT_SECRET"
EnvAccessToken = envNamespace + "ACCESS_TOKEN"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
2021-02-21 10:56:56 +00:00
)
2020-07-08 20:16:54 +00:00
2021-02-21 10:56:56 +00:00
const (
defaultPropagationTimeout = 3 * time . Minute
defaultPollInterval = 15 * time . Second
2020-07-08 20:16:54 +00:00
)
2021-02-21 10:56:56 +00:00
const maxBody = 131072
2020-07-08 20:16:54 +00:00
// Config is used to configure the creation of the DNSProvider.
type Config struct {
edgegrid . Config
PropagationTimeout time . Duration
PollingInterval time . Duration
TTL int
}
// NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig ( ) * Config {
return & Config {
TTL : env . GetOrDefaultInt ( EnvTTL , dns01 . DefaultTTL ) ,
2021-02-21 10:56:56 +00:00
PropagationTimeout : env . GetOrDefaultSecond ( EnvPropagationTimeout , defaultPropagationTimeout ) ,
PollingInterval : env . GetOrDefaultSecond ( EnvPollingInterval , defaultPollInterval ) ,
Config : edgegrid . Config { MaxBody : maxBody } ,
2020-07-08 20:16:54 +00:00
}
}
// DNSProvider implements the challenge.Provider interface.
type DNSProvider struct {
config * Config
}
2021-02-21 10:56:56 +00:00
// NewDNSProvider returns a DNSProvider instance configured for Akamai EdgeDNS:
// Akamai credentials are automatically detected in the following locations and prioritized in the following order:
//
// 1. Section-specific environment variables `AKAMAI_{SECTION}_HOST`, `AKAMAI_{SECTION}_ACCESS_TOKEN`, `AKAMAI_{SECTION}_CLIENT_TOKEN`, `AKAMAI_{SECTION}_CLIENT_SECRET` where `{SECTION}` is specified using `AKAMAI_EDGERC_SECTION`
// 2. If `AKAMAI_EDGERC_SECTION` is not defined or is set to `default`: Environment variables `AKAMAI_HOST`, `AKAMAI_ACCESS_TOKEN`, `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`
// 3. .edgerc file located at `AKAMAI_EDGERC` (defaults to `~/.edgerc`, sections can be specified using `AKAMAI_EDGERC_SECTION`)
// 4. Default environment variables: `AKAMAI_HOST`, `AKAMAI_ACCESS_TOKEN`, `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET`
//
// See also: https://developer.akamai.com/api/getting-started
2020-07-08 20:16:54 +00:00
func NewDNSProvider ( ) ( * DNSProvider , error ) {
2021-02-21 10:56:56 +00:00
config := NewDefaultConfig ( )
rcPath := env . GetOrDefaultString ( EnvEdgeRc , "" )
rcSection := env . GetOrDefaultString ( EnvEdgeRcSection , "" )
conf , err := edgegrid . Init ( rcPath , rcSection )
2020-07-08 20:16:54 +00:00
if err != nil {
return nil , fmt . Errorf ( "edgedns: %w" , err )
}
2021-02-21 10:56:56 +00:00
conf . MaxBody = maxBody
config . Config = conf
2020-07-08 20:16:54 +00:00
return NewDNSProviderConfig ( config )
}
// NewDNSProviderConfig return a DNSProvider instance configured for EdgeDNS.
func NewDNSProviderConfig ( config * Config ) ( * DNSProvider , error ) {
if config == nil {
return nil , errors . New ( "edgedns: the configuration of the DNS provider is nil" )
}
2020-08-09 09:18:23 +00:00
configdns . Init ( config . Config )
2020-07-08 20:16:54 +00:00
return & DNSProvider { config : config } , nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func ( d * DNSProvider ) Timeout ( ) ( timeout , interval time . Duration ) {
return d . config . PropagationTimeout , d . config . PollingInterval
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func ( d * DNSProvider ) Present ( domain , token , keyAuth string ) error {
fqdn , value := dns01 . GetRecord ( domain , keyAuth )
zone , err := findZone ( domain )
if err != nil {
return fmt . Errorf ( "edgedns: %w" , err )
}
record , err := configdns . GetRecord ( zone , fqdn , "TXT" )
if err != nil && ! isNotFound ( err ) {
return fmt . Errorf ( "edgedns: %w" , err )
}
if err == nil && record == nil {
return fmt . Errorf ( "edgedns: unknown error" )
}
if record != nil {
log . Infof ( "TXT record already exists. Updating target" )
if containsValue ( record . Target , value ) {
// have a record and have entry already
return nil
}
record . Target = append ( record . Target , ` " ` + value + ` " ` )
record . TTL = d . config . TTL
err = record . Update ( zone )
if err != nil {
return fmt . Errorf ( "edgedns: %w" , err )
}
}
record = & configdns . RecordBody {
Name : fqdn ,
RecordType : "TXT" ,
TTL : d . config . TTL ,
Target : [ ] string { ` " ` + value + ` " ` } ,
}
err = record . Save ( zone )
if err != nil {
return fmt . Errorf ( "edgedns: %w" , err )
}
return nil
}
// CleanUp removes the record matching the specified parameters.
func ( d * DNSProvider ) CleanUp ( domain , token , keyAuth string ) error {
fqdn , value := dns01 . GetRecord ( domain , keyAuth )
zone , err := findZone ( domain )
if err != nil {
return fmt . Errorf ( "edgedns: %w" , err )
}
existingRec , err := configdns . GetRecord ( zone , fqdn , "TXT" )
if err != nil {
if isNotFound ( err ) {
return nil
}
return fmt . Errorf ( "edgedns: %w" , err )
}
if existingRec == nil {
return fmt . Errorf ( "edgedns: unknown failure" )
}
if len ( existingRec . Target ) == 0 {
return fmt . Errorf ( "edgedns: TXT record is invalid" )
}
if ! containsValue ( existingRec . Target , value ) {
return nil
}
var newRData [ ] string
for _ , val := range existingRec . Target {
val = strings . Trim ( val , ` " ` )
if val == value {
continue
}
newRData = append ( newRData , val )
}
if len ( newRData ) > 0 {
existingRec . Target = newRData
err = existingRec . Update ( zone )
if err != nil {
return fmt . Errorf ( "edgedns: %w" , err )
}
}
err = existingRec . Delete ( zone )
if err != nil {
return fmt . Errorf ( "edgedns: %w" , err )
}
return nil
}
func findZone ( domain string ) ( string , error ) {
zone , err := dns01 . FindZoneByFqdn ( dns01 . ToFqdn ( domain ) )
if err != nil {
return "" , err
}
return dns01 . UnFqdn ( zone ) , nil
}
func containsValue ( values [ ] string , value string ) bool {
for _ , val := range values {
if strings . Trim ( val , ` " ` ) == value {
return true
}
}
return false
}
func isNotFound ( err error ) bool {
if err == nil {
return false
}
2020-10-27 11:01:05 +00:00
var e configdns . ConfigDNSError
return errors . As ( err , & e ) && e . NotFound ( )
2020-07-08 20:16:54 +00:00
}