godaddy.go raw
1 // Package godaddy implements a DNS provider for solving the DNS-01 challenge using godaddy DNS.
2 package godaddy
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "time"
10
11 "github.com/go-acme/lego/v4/challenge"
12 "github.com/go-acme/lego/v4/challenge/dns01"
13 "github.com/go-acme/lego/v4/platform/config/env"
14 "github.com/go-acme/lego/v4/providers/dns/godaddy/internal"
15 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
16 )
17
18 // Environment variables names.
19 const (
20 envNamespace = "GODADDY_"
21
22 EnvAPIKey = envNamespace + "API_KEY"
23 EnvAPISecret = envNamespace + "API_SECRET"
24
25 EnvTTL = envNamespace + "TTL"
26 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
27 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
28 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
29 )
30
31 const minTTL = 600
32
33 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
34
35 // Config is used to configure the creation of the DNSProvider.
36 type Config struct {
37 APIKey string
38 APISecret string
39 PropagationTimeout time.Duration
40 PollingInterval time.Duration
41 TTL int
42 HTTPClient *http.Client
43 }
44
45 // NewDefaultConfig returns a default configuration for the DNSProvider.
46 func NewDefaultConfig() *Config {
47 return &Config{
48 TTL: env.GetOrDefaultInt(EnvTTL, minTTL),
49 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),
50 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
51 HTTPClient: &http.Client{
52 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
53 },
54 }
55 }
56
57 // DNSProvider implements the challenge.Provider interface.
58 type DNSProvider struct {
59 config *Config
60 client *internal.Client
61 }
62
63 // NewDNSProvider returns a DNSProvider instance configured for godaddy.
64 // Credentials must be passed in the environment variables:
65 // GODADDY_API_KEY and GODADDY_API_SECRET.
66 func NewDNSProvider() (*DNSProvider, error) {
67 values, err := env.Get(EnvAPIKey, EnvAPISecret)
68 if err != nil {
69 return nil, fmt.Errorf("godaddy: %w", err)
70 }
71
72 config := NewDefaultConfig()
73 config.APIKey = values[EnvAPIKey]
74 config.APISecret = values[EnvAPISecret]
75
76 return NewDNSProviderConfig(config)
77 }
78
79 // NewDNSProviderConfig return a DNSProvider instance configured for godaddy.
80 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
81 if config == nil {
82 return nil, errors.New("godaddy: the configuration of the DNS provider is nil")
83 }
84
85 if config.APIKey == "" || config.APISecret == "" {
86 return nil, errors.New("godaddy: credentials missing")
87 }
88
89 if config.TTL < minTTL {
90 return nil, fmt.Errorf("godaddy: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
91 }
92
93 client := internal.NewClient(config.APIKey, config.APISecret)
94
95 if config.HTTPClient != nil {
96 client.HTTPClient = config.HTTPClient
97 }
98
99 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
100
101 return &DNSProvider{config: config, client: client}, nil
102 }
103
104 // Timeout returns the timeout and interval to use when checking for DNS propagation.
105 // Adjusting here to cope with spikes in propagation times.
106 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
107 return d.config.PropagationTimeout, d.config.PollingInterval
108 }
109
110 // Present creates a TXT record to fulfill the dns-01 challenge.
111 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
112 info := dns01.GetChallengeInfo(domain, keyAuth)
113
114 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
115 if err != nil {
116 return fmt.Errorf("godaddy: could not find zone for domain %q: %w", domain, err)
117 }
118
119 authZone = dns01.UnFqdn(authZone)
120
121 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
122 if err != nil {
123 return fmt.Errorf("godaddy: %w", err)
124 }
125
126 ctx := context.Background()
127
128 existingRecords, err := d.client.GetRecords(ctx, authZone, "TXT", subDomain)
129 if err != nil {
130 return fmt.Errorf("godaddy: failed to get TXT records: %w", err)
131 }
132
133 var newRecords []internal.DNSRecord
134
135 for _, record := range existingRecords {
136 if record.Data != "" {
137 newRecords = append(newRecords, record)
138 }
139 }
140
141 record := internal.DNSRecord{
142 Type: "TXT",
143 Name: subDomain,
144 Data: info.Value,
145 TTL: d.config.TTL,
146 }
147 newRecords = append(newRecords, record)
148
149 err = d.client.UpdateTxtRecords(ctx, newRecords, authZone, subDomain)
150 if err != nil {
151 return fmt.Errorf("godaddy: failed to add TXT record: %w", err)
152 }
153
154 return nil
155 }
156
157 // CleanUp removes the record matching the specified parameters.
158 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
159 info := dns01.GetChallengeInfo(domain, keyAuth)
160
161 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
162 if err != nil {
163 return fmt.Errorf("godaddy: could not find zone for domain %q: %w", domain, err)
164 }
165
166 authZone = dns01.UnFqdn(authZone)
167
168 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
169 if err != nil {
170 return fmt.Errorf("godaddy: %w", err)
171 }
172
173 ctx := context.Background()
174
175 existingRecords, err := d.client.GetRecords(ctx, authZone, "TXT", subDomain)
176 if err != nil {
177 return fmt.Errorf("godaddy: failed to get all TXT records: %w", err)
178 }
179
180 var recordsToKeep []internal.DNSRecord
181
182 for _, record := range existingRecords {
183 if record.Data != info.Value && record.Data != "" {
184 recordsToKeep = append(recordsToKeep, record)
185 }
186 }
187
188 if len(recordsToKeep) == 0 {
189 err = d.client.DeleteTxtRecords(ctx, authZone, subDomain)
190 if err != nil {
191 return fmt.Errorf("godaddy: failed to delete TXT record: %w", err)
192 }
193
194 return nil
195 }
196
197 err = d.client.UpdateTxtRecords(ctx, recordsToKeep, authZone, subDomain)
198 if err != nil {
199 return fmt.Errorf("godaddy: failed to remove TXT record: %w", err)
200 }
201
202 return nil
203 }
204