1// Package constellix implements a DNS provider for solving the DNS-01 challenge using Constellix DNS. 2package constellix 3 4import ( 5 "errors" 6 "fmt" 7 "net/http" 8 "time" 9 10 "github.com/go-acme/lego/v3/challenge/dns01" 11 "github.com/go-acme/lego/v3/platform/config/env" 12 "github.com/go-acme/lego/v3/providers/dns/constellix/internal" 13) 14 15// Environment variables names. 16const ( 17 envNamespace = "CONSTELLIX_" 18 19 EnvAPIKey = envNamespace + "API_KEY" 20 EnvSecretKey = envNamespace + "SECRET_KEY" 21 22 EnvTTL = envNamespace + "TTL" 23 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" 24 EnvPollingInterval = envNamespace + "POLLING_INTERVAL" 25 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" 26) 27 28// Config is used to configure the creation of the DNSProvider. 29type Config struct { 30 APIKey string 31 SecretKey string 32 PropagationTimeout time.Duration 33 PollingInterval time.Duration 34 TTL int 35 HTTPClient *http.Client 36} 37 38// NewDefaultConfig returns a default configuration for the DNSProvider. 39func NewDefaultConfig() *Config { 40 return &Config{ 41 TTL: env.GetOrDefaultInt(EnvTTL, 300), 42 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), 43 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), 44 HTTPClient: &http.Client{ 45 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), 46 }, 47 } 48} 49 50// DNSProvider implements the challenge.Provider interface. 51type DNSProvider struct { 52 config *Config 53 client *internal.Client 54} 55 56// NewDNSProvider returns a DNSProvider instance configured for Constellix. 57// Credentials must be passed in the environment variables: 58// CONSTELLIX_API_KEY and CONSTELLIX_SECRET_KEY. 59func NewDNSProvider() (*DNSProvider, error) { 60 values, err := env.Get(EnvAPIKey, EnvSecretKey) 61 if err != nil { 62 return nil, fmt.Errorf("constellix: %w", err) 63 } 64 65 config := NewDefaultConfig() 66 config.APIKey = values[EnvAPIKey] 67 config.SecretKey = values[EnvSecretKey] 68 69 return NewDNSProviderConfig(config) 70} 71 72// NewDNSProviderConfig return a DNSProvider instance configured for Constellix. 73func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { 74 if config == nil { 75 return nil, errors.New("constellix: the configuration of the DNS provider is nil") 76 } 77 78 if config.SecretKey == "" || config.APIKey == "" { 79 return nil, errors.New("constellix: incomplete credentials, missing secret key and/or API key") 80 } 81 82 tr, err := internal.NewTokenTransport(config.APIKey, config.SecretKey) 83 if err != nil { 84 return nil, fmt.Errorf("constellix: %w", err) 85 } 86 87 client := internal.NewClient(tr.Wrap(config.HTTPClient)) 88 89 return &DNSProvider{config: config, client: client}, nil 90} 91 92// Timeout returns the timeout and interval to use when checking for DNS propagation. 93// Adjusting here to cope with spikes in propagation times. 94func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { 95 return d.config.PropagationTimeout, d.config.PollingInterval 96} 97 98// Present creates a TXT record using the specified parameters. 99func (d *DNSProvider) Present(domain, token, keyAuth string) error { 100 fqdn, value := dns01.GetRecord(domain, keyAuth) 101 102 authZone, err := dns01.FindZoneByFqdn(fqdn) 103 if err != nil { 104 return fmt.Errorf("constellix: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err) 105 } 106 107 dom, err := d.client.Domains.GetByName(dns01.UnFqdn(authZone)) 108 if err != nil { 109 return fmt.Errorf("constellix: failed to get domain: %w", err) 110 } 111 112 recordName := getRecordName(fqdn, authZone) 113 114 records, err := d.client.TxtRecords.Search(dom.ID, internal.Exact, recordName) 115 if err != nil { 116 return fmt.Errorf("constellix: failed to get TXT records: %w", err) 117 } 118 119 if len(records) > 1 { 120 return errors.New("constellix: failed to get TXT records") 121 } 122 123 // TXT record entry already existing 124 if len(records) == 1 { 125 record := records[0] 126 127 if containsValue(record, value) { 128 return nil 129 } 130 131 request := internal.RecordRequest{ 132 Name: record.Name, 133 TTL: record.TTL, 134 RoundRobin: append(record.RoundRobin, internal.RecordValue{Value: fmt.Sprintf(`"%s"`, value)}), 135 } 136 137 _, err = d.client.TxtRecords.Update(dom.ID, record.ID, request) 138 if err != nil { 139 return fmt.Errorf("constellix: failed to update TXT records: %w", err) 140 } 141 return nil 142 } 143 144 request := internal.RecordRequest{ 145 Name: recordName, 146 TTL: d.config.TTL, 147 RoundRobin: []internal.RecordValue{ 148 {Value: fmt.Sprintf(`"%s"`, value)}, 149 }, 150 } 151 152 _, err = d.client.TxtRecords.Create(dom.ID, request) 153 if err != nil { 154 return fmt.Errorf("constellix: failed to create TXT record %s: %w", fqdn, err) 155 } 156 157 return nil 158} 159 160// CleanUp removes the TXT record matching the specified parameters. 161func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { 162 fqdn, value := dns01.GetRecord(domain, keyAuth) 163 164 authZone, err := dns01.FindZoneByFqdn(fqdn) 165 if err != nil { 166 return fmt.Errorf("constellix: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err) 167 } 168 169 dom, err := d.client.Domains.GetByName(dns01.UnFqdn(authZone)) 170 if err != nil { 171 return fmt.Errorf("constellix: failed to get domain: %w", err) 172 } 173 174 recordName := getRecordName(fqdn, authZone) 175 176 records, err := d.client.TxtRecords.Search(dom.ID, internal.Exact, recordName) 177 if err != nil { 178 return fmt.Errorf("constellix: failed to get TXT records: %w", err) 179 } 180 181 if len(records) > 1 { 182 return errors.New("constellix: failed to get TXT records") 183 } 184 185 if len(records) == 0 { 186 return nil 187 } 188 189 record := records[0] 190 191 if !containsValue(record, value) { 192 return nil 193 } 194 195 // only 1 record value, the whole record must be deleted. 196 if len(record.Value) == 1 { 197 _, err = d.client.TxtRecords.Delete(dom.ID, record.ID) 198 if err != nil { 199 return fmt.Errorf("constellix: failed to delete TXT records: %w", err) 200 } 201 return nil 202 } 203 204 request := internal.RecordRequest{ 205 Name: record.Name, 206 TTL: record.TTL, 207 } 208 209 for _, val := range record.Value { 210 if val.Value != fmt.Sprintf(`"%s"`, value) { 211 request.RoundRobin = append(request.RoundRobin, val) 212 } 213 } 214 215 _, err = d.client.TxtRecords.Update(dom.ID, record.ID, request) 216 if err != nil { 217 return fmt.Errorf("constellix: failed to update TXT records: %w", err) 218 } 219 220 return nil 221} 222 223func containsValue(record internal.Record, value string) bool { 224 for _, val := range record.Value { 225 if val.Value == fmt.Sprintf(`"%s"`, value) { 226 return true 227 } 228 } 229 230 return false 231} 232 233func getRecordName(fqdn, authZone string) string { 234 return fqdn[0 : len(fqdn)-len(authZone)-1] 235} 236