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