1// Package edgedns replaces fastdns, implementing a DNS provider for solving the DNS-01 challenge using Akamai EdgeDNS. 2package edgedns 3 4import ( 5 "errors" 6 "fmt" 7 "strings" 8 "time" 9 10 configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2" 11 "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" 12 "github.com/go-acme/lego/v4/challenge/dns01" 13 "github.com/go-acme/lego/v4/log" 14 "github.com/go-acme/lego/v4/platform/config/env" 15) 16 17// Environment variables names. 18const ( 19 envNamespace = "AKAMAI_" 20 21 EnvEdgeRc = envNamespace + "EDGERC" 22 EnvEdgeRcSection = envNamespace + "EDGERC_SECTION" 23 24 EnvHost = envNamespace + "HOST" 25 EnvClientToken = envNamespace + "CLIENT_TOKEN" 26 EnvClientSecret = envNamespace + "CLIENT_SECRET" 27 EnvAccessToken = envNamespace + "ACCESS_TOKEN" 28 29 EnvTTL = envNamespace + "TTL" 30 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" 31 EnvPollingInterval = envNamespace + "POLLING_INTERVAL" 32) 33 34const ( 35 defaultPropagationTimeout = 3 * time.Minute 36 defaultPollInterval = 15 * time.Second 37) 38 39const maxBody = 131072 40 41// Config is used to configure the creation of the DNSProvider. 42type Config struct { 43 edgegrid.Config 44 PropagationTimeout time.Duration 45 PollingInterval time.Duration 46 TTL int 47} 48 49// NewDefaultConfig returns a default configuration for the DNSProvider. 50func NewDefaultConfig() *Config { 51 return &Config{ 52 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), 53 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), 54 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollInterval), 55 Config: edgegrid.Config{MaxBody: maxBody}, 56 } 57} 58 59// DNSProvider implements the challenge.Provider interface. 60type DNSProvider struct { 61 config *Config 62} 63 64// NewDNSProvider returns a DNSProvider instance configured for Akamai EdgeDNS: 65// Akamai credentials are automatically detected in the following locations and prioritized in the following order: 66// 67// 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` 68// 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` 69// 3. .edgerc file located at `AKAMAI_EDGERC` (defaults to `~/.edgerc`, sections can be specified using `AKAMAI_EDGERC_SECTION`) 70// 4. Default environment variables: `AKAMAI_HOST`, `AKAMAI_ACCESS_TOKEN`, `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET` 71// 72// See also: https://developer.akamai.com/api/getting-started 73func NewDNSProvider() (*DNSProvider, error) { 74 config := NewDefaultConfig() 75 76 rcPath := env.GetOrDefaultString(EnvEdgeRc, "") 77 rcSection := env.GetOrDefaultString(EnvEdgeRcSection, "") 78 79 conf, err := edgegrid.Init(rcPath, rcSection) 80 if err != nil { 81 return nil, fmt.Errorf("edgedns: %w", err) 82 } 83 84 conf.MaxBody = maxBody 85 86 config.Config = conf 87 88 return NewDNSProviderConfig(config) 89} 90 91// NewDNSProviderConfig return a DNSProvider instance configured for EdgeDNS. 92func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { 93 if config == nil { 94 return nil, errors.New("edgedns: the configuration of the DNS provider is nil") 95 } 96 97 configdns.Init(config.Config) 98 99 return &DNSProvider{config: config}, nil 100} 101 102// Timeout returns the timeout and interval to use when checking for DNS propagation. 103// Adjusting here to cope with spikes in propagation times. 104func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { 105 return d.config.PropagationTimeout, d.config.PollingInterval 106} 107 108// Present creates a TXT record to fulfill the dns-01 challenge. 109func (d *DNSProvider) Present(domain, token, keyAuth string) error { 110 fqdn, value := dns01.GetRecord(domain, keyAuth) 111 112 zone, err := findZone(domain) 113 if err != nil { 114 return fmt.Errorf("edgedns: %w", err) 115 } 116 117 record, err := configdns.GetRecord(zone, fqdn, "TXT") 118 if err != nil && !isNotFound(err) { 119 return fmt.Errorf("edgedns: %w", err) 120 } 121 122 if err == nil && record == nil { 123 return fmt.Errorf("edgedns: unknown error") 124 } 125 126 if record != nil { 127 log.Infof("TXT record already exists. Updating target") 128 129 if containsValue(record.Target, value) { 130 // have a record and have entry already 131 return nil 132 } 133 134 record.Target = append(record.Target, `"`+value+`"`) 135 record.TTL = d.config.TTL 136 137 err = record.Update(zone) 138 if err != nil { 139 return fmt.Errorf("edgedns: %w", err) 140 } 141 142 return nil 143 } 144 145 record = &configdns.RecordBody{ 146 Name: fqdn, 147 RecordType: "TXT", 148 TTL: d.config.TTL, 149 Target: []string{`"` + value + `"`}, 150 } 151 152 err = record.Save(zone) 153 if err != nil { 154 return fmt.Errorf("edgedns: %w", err) 155 } 156 157 return nil 158} 159 160// CleanUp removes the record matching the specified parameters. 161func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { 162 fqdn, value := dns01.GetRecord(domain, keyAuth) 163 164 zone, err := findZone(domain) 165 if err != nil { 166 return fmt.Errorf("edgedns: %w", err) 167 } 168 169 existingRec, err := configdns.GetRecord(zone, fqdn, "TXT") 170 if err != nil { 171 if isNotFound(err) { 172 return nil 173 } 174 return fmt.Errorf("edgedns: %w", err) 175 } 176 177 if existingRec == nil { 178 return fmt.Errorf("edgedns: unknown failure") 179 } 180 181 if len(existingRec.Target) == 0 { 182 return fmt.Errorf("edgedns: TXT record is invalid") 183 } 184 185 if !containsValue(existingRec.Target, value) { 186 return nil 187 } 188 189 var newRData []string 190 for _, val := range existingRec.Target { 191 val = strings.Trim(val, `"`) 192 if val == value { 193 continue 194 } 195 newRData = append(newRData, val) 196 } 197 198 if len(newRData) > 0 { 199 existingRec.Target = newRData 200 201 err = existingRec.Update(zone) 202 if err != nil { 203 return fmt.Errorf("edgedns: %w", err) 204 } 205 206 return nil 207 } 208 209 err = existingRec.Delete(zone) 210 if err != nil { 211 return fmt.Errorf("edgedns: %w", err) 212 } 213 214 return nil 215} 216 217func findZone(domain string) (string, error) { 218 zone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) 219 if err != nil { 220 return "", err 221 } 222 223 return dns01.UnFqdn(zone), nil 224} 225 226func containsValue(values []string, value string) bool { 227 for _, val := range values { 228 if strings.Trim(val, `"`) == value { 229 return true 230 } 231 } 232 233 return false 234} 235 236func isNotFound(err error) bool { 237 if err == nil { 238 return false 239 } 240 241 var e configdns.ConfigDNSError 242 return errors.As(err, &e) && e.NotFound() 243} 244