easydns.go raw
1 // Package easydns implements a DNS provider for solving the DNS-01 challenge using EasyDNS API.
2 package easydns
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "net/url"
10 "strconv"
11 "strings"
12 "sync"
13 "time"
14
15 "github.com/go-acme/lego/v4/challenge"
16 "github.com/go-acme/lego/v4/challenge/dns01"
17 "github.com/go-acme/lego/v4/platform/config/env"
18 "github.com/go-acme/lego/v4/providers/dns/easydns/internal"
19 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
20 )
21
22 // Environment variables names.
23 const (
24 envNamespace = "EASYDNS_"
25
26 EnvEndpoint = envNamespace + "ENDPOINT"
27 EnvToken = envNamespace + "TOKEN"
28 EnvKey = envNamespace + "KEY"
29
30 EnvTTL = envNamespace + "TTL"
31 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
32 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
33 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
34 EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL"
35 )
36
37 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
38
39 // Config is used to configure the creation of the DNSProvider.
40 type Config struct {
41 Endpoint *url.URL
42 Token string
43 Key string
44 TTL int
45 HTTPClient *http.Client
46 PropagationTimeout time.Duration
47 PollingInterval time.Duration
48 SequenceInterval time.Duration
49 }
50
51 // NewDefaultConfig returns a default configuration for the DNSProvider.
52 func NewDefaultConfig() *Config {
53 return &Config{
54 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
55 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
56 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
57 SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),
58 HTTPClient: &http.Client{
59 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
60 },
61 }
62 }
63
64 // DNSProvider implements the challenge.Provider interface.
65 type DNSProvider struct {
66 config *Config
67 client *internal.Client
68
69 recordIDs map[string]string
70 recordIDsMu sync.Mutex
71 }
72
73 // NewDNSProvider returns a DNSProvider instance.
74 func NewDNSProvider() (*DNSProvider, error) {
75 config := NewDefaultConfig()
76
77 endpoint, err := url.Parse(env.GetOrDefaultString(EnvEndpoint, internal.DefaultBaseURL))
78 if err != nil {
79 return nil, fmt.Errorf("easydns: %w", err)
80 }
81
82 config.Endpoint = endpoint
83
84 values, err := env.Get(EnvToken, EnvKey)
85 if err != nil {
86 return nil, fmt.Errorf("easydns: %w", err)
87 }
88
89 config.Token = values[EnvToken]
90 config.Key = values[EnvKey]
91
92 return NewDNSProviderConfig(config)
93 }
94
95 // NewDNSProviderConfig return a DNSProvider instance configured for EasyDNS.
96 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
97 if config == nil {
98 return nil, errors.New("easydns: the configuration of the DNS provider is nil")
99 }
100
101 if config.Token == "" {
102 return nil, errors.New("easydns: the API token is missing")
103 }
104
105 if config.Key == "" {
106 return nil, errors.New("easydns: the API key is missing")
107 }
108
109 client := internal.NewClient(config.Token, config.Key)
110
111 if config.HTTPClient != nil {
112 client.HTTPClient = config.HTTPClient
113 }
114
115 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
116
117 if config.Endpoint != nil {
118 client.BaseURL = config.Endpoint
119 }
120
121 return &DNSProvider{config: config, client: client, recordIDs: map[string]string{}}, nil
122 }
123
124 // Present creates a TXT record to fulfill the dns-01 challenge.
125 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
126 ctx := context.Background()
127
128 info := dns01.GetChallengeInfo(domain, keyAuth)
129
130 authZone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN))
131 if err != nil {
132 return fmt.Errorf("easydns: %w", err)
133 }
134
135 if authZone == "" {
136 return fmt.Errorf("easydns: could not find zone for domain %q", domain)
137 }
138
139 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
140 if err != nil {
141 return fmt.Errorf("easydns: %w", err)
142 }
143
144 record := internal.ZoneRecord{
145 Domain: authZone,
146 Host: subDomain,
147 Type: "TXT",
148 Rdata: info.Value,
149 TTL: strconv.Itoa(d.config.TTL),
150 Priority: "0",
151 }
152
153 recordID, err := d.client.AddRecord(ctx, dns01.UnFqdn(authZone), record)
154 if err != nil {
155 return fmt.Errorf("easydns: error adding zone record: %w", err)
156 }
157
158 key := getMapKey(info.EffectiveFQDN, info.Value)
159
160 d.recordIDsMu.Lock()
161 d.recordIDs[key] = recordID
162 d.recordIDsMu.Unlock()
163
164 return nil
165 }
166
167 // CleanUp removes the TXT record matching the specified parameters.
168 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
169 ctx := context.Background()
170
171 info := dns01.GetChallengeInfo(domain, keyAuth)
172
173 key := getMapKey(info.EffectiveFQDN, info.Value)
174
175 d.recordIDsMu.Lock()
176 recordID, exists := d.recordIDs[key]
177 d.recordIDsMu.Unlock()
178
179 if !exists {
180 return nil
181 }
182
183 authZone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN))
184 if err != nil {
185 return fmt.Errorf("easydns: %w", err)
186 }
187
188 if authZone == "" {
189 return fmt.Errorf("easydns: could not find zone for domain %q", domain)
190 }
191
192 err = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), recordID)
193 if err != nil {
194 return fmt.Errorf("easydns: %w", err)
195 }
196
197 d.recordIDsMu.Lock()
198 delete(d.recordIDs, key)
199 d.recordIDsMu.Unlock()
200
201 return nil
202 }
203
204 // Timeout returns the timeout and interval to use when checking for DNS propagation.
205 // Adjusting here to cope with spikes in propagation times.
206 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
207 return d.config.PropagationTimeout, d.config.PollingInterval
208 }
209
210 // Sequential All DNS challenges for this provider will be resolved sequentially.
211 // Returns the interval between each iteration.
212 func (d *DNSProvider) Sequential() time.Duration {
213 return d.config.SequenceInterval
214 }
215
216 func getMapKey(fqdn, value string) string {
217 return fqdn + "|" + value
218 }
219
220 func (d *DNSProvider) findZone(ctx context.Context, domain string) (string, error) {
221 var errAll error
222
223 for {
224 i := strings.Index(domain, ".")
225 if i == -1 {
226 break
227 }
228
229 _, err := d.client.ListZones(ctx, domain)
230 if err == nil {
231 return domain, nil
232 }
233
234 errAll = errors.Join(errAll, err)
235
236 domain = domain[i+1:]
237 }
238
239 return "", errAll
240 }
241