1// Package dyn implements a DNS provider for solving the DNS-01 challenge using Dyn Managed DNS. 2package dyn 3 4import ( 5 "errors" 6 "fmt" 7 "net/http" 8 "strconv" 9 "time" 10 11 "github.com/go-acme/lego/v4/challenge/dns01" 12 "github.com/go-acme/lego/v4/platform/config/env" 13) 14 15// Environment variables names. 16const ( 17 envNamespace = "DYN_" 18 19 EnvCustomerName = envNamespace + "CUSTOMER_NAME" 20 EnvUserName = envNamespace + "USER_NAME" 21 EnvPassword = envNamespace + "PASSWORD" 22 23 EnvTTL = envNamespace + "TTL" 24 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" 25 EnvPollingInterval = envNamespace + "POLLING_INTERVAL" 26 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" 27) 28 29// Config is used to configure the creation of the DNSProvider. 30type Config struct { 31 CustomerName string 32 UserName string 33 Password string 34 HTTPClient *http.Client 35 PropagationTimeout time.Duration 36 PollingInterval time.Duration 37 TTL int 38} 39 40// NewDefaultConfig returns a default configuration for the DNSProvider. 41func NewDefaultConfig() *Config { 42 return &Config{ 43 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), 44 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), 45 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), 46 HTTPClient: &http.Client{ 47 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), 48 }, 49 } 50} 51 52// DNSProvider implements the challenge.Provider interface. 53type DNSProvider struct { 54 config *Config 55 token string 56} 57 58// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS. 59// Credentials must be passed in the environment variables: 60// DYN_CUSTOMER_NAME, DYN_USER_NAME and DYN_PASSWORD. 61func NewDNSProvider() (*DNSProvider, error) { 62 values, err := env.Get(EnvCustomerName, EnvUserName, EnvPassword) 63 if err != nil { 64 return nil, fmt.Errorf("dyn: %w", err) 65 } 66 67 config := NewDefaultConfig() 68 config.CustomerName = values[EnvCustomerName] 69 config.UserName = values[EnvUserName] 70 config.Password = values[EnvPassword] 71 72 return NewDNSProviderConfig(config) 73} 74 75// NewDNSProviderConfig return a DNSProvider instance configured for Dyn DNS. 76func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { 77 if config == nil { 78 return nil, errors.New("dyn: the configuration of the DNS provider is nil") 79 } 80 81 if config.CustomerName == "" || config.UserName == "" || config.Password == "" { 82 return nil, errors.New("dyn: credentials missing") 83 } 84 85 return &DNSProvider{config: config}, nil 86} 87 88// Present creates a TXT record using the specified parameters. 89func (d *DNSProvider) Present(domain, token, keyAuth string) error { 90 fqdn, value := dns01.GetRecord(domain, keyAuth) 91 92 authZone, err := dns01.FindZoneByFqdn(fqdn) 93 if err != nil { 94 return fmt.Errorf("dyn: %w", err) 95 } 96 97 err = d.login() 98 if err != nil { 99 return fmt.Errorf("dyn: %w", err) 100 } 101 102 data := map[string]interface{}{ 103 "rdata": map[string]string{ 104 "txtdata": value, 105 }, 106 "ttl": strconv.Itoa(d.config.TTL), 107 } 108 109 resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn) 110 _, err = d.sendRequest(http.MethodPost, resource, data) 111 if err != nil { 112 return fmt.Errorf("dyn: %w", err) 113 } 114 115 err = d.publish(authZone, "Added TXT record for ACME dns-01 challenge using lego client") 116 if err != nil { 117 return fmt.Errorf("dyn: %w", err) 118 } 119 120 return d.logout() 121} 122 123// CleanUp removes the TXT record matching the specified parameters. 124func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { 125 fqdn, _ := dns01.GetRecord(domain, keyAuth) 126 127 authZone, err := dns01.FindZoneByFqdn(fqdn) 128 if err != nil { 129 return fmt.Errorf("dyn: %w", err) 130 } 131 132 err = d.login() 133 if err != nil { 134 return fmt.Errorf("dyn: %w", err) 135 } 136 137 resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn) 138 url := fmt.Sprintf("%s/%s", defaultBaseURL, resource) 139 140 req, err := http.NewRequest(http.MethodDelete, url, nil) 141 if err != nil { 142 return fmt.Errorf("dyn: %w", err) 143 } 144 145 req.Header.Set("Content-Type", "application/json") 146 req.Header.Set("Auth-Token", d.token) 147 148 resp, err := d.config.HTTPClient.Do(req) 149 if err != nil { 150 return fmt.Errorf("dyn: %w", err) 151 } 152 resp.Body.Close() 153 154 if resp.StatusCode != http.StatusOK { 155 return fmt.Errorf("dyn: API request failed to delete TXT record HTTP status code %d", resp.StatusCode) 156 } 157 158 err = d.publish(authZone, "Removed TXT record for ACME dns-01 challenge using lego client") 159 if err != nil { 160 return fmt.Errorf("dyn: %w", err) 161 } 162 163 return d.logout() 164} 165 166// Timeout returns the timeout and interval to use when checking for DNS propagation. 167// Adjusting here to cope with spikes in propagation times. 168func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { 169 return d.config.PropagationTimeout, d.config.PollingInterval 170} 171