gandiv5.go raw
1 // Package gandiv5 implements a DNS provider for solving the DNS-01 challenge using Gandi LiveDNS api.
2 package gandiv5
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "net/url"
10 "sync"
11 "time"
12
13 "github.com/go-acme/lego/v4/challenge"
14 "github.com/go-acme/lego/v4/challenge/dns01"
15 "github.com/go-acme/lego/v4/log"
16 "github.com/go-acme/lego/v4/platform/config/env"
17 "github.com/go-acme/lego/v4/providers/dns/gandiv5/internal"
18 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
19 )
20
21 // Environment variables names.
22 const (
23 envNamespace = "GANDIV5_"
24
25 EnvAPIKey = envNamespace + "API_KEY"
26 EnvPersonalAccessToken = envNamespace + "PERSONAL_ACCESS_TOKEN"
27
28 EnvTTL = envNamespace + "TTL"
29 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
30 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
31 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
32 )
33
34 const minTTL = 300
35
36 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
37
38 // inProgressInfo contains information about an in-progress challenge.
39 type inProgressInfo struct {
40 fieldName string
41 authZone string
42 }
43
44 // Config is used to configure the creation of the DNSProvider.
45 type Config struct {
46 BaseURL string
47 APIKey string // Deprecated use PersonalAccessToken
48 PersonalAccessToken string
49 PropagationTimeout time.Duration
50 PollingInterval time.Duration
51 TTL int
52 HTTPClient *http.Client
53 }
54
55 // NewDefaultConfig returns a default configuration for the DNSProvider.
56 func NewDefaultConfig() *Config {
57 return &Config{
58 TTL: env.GetOrDefaultInt(EnvTTL, minTTL),
59 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 20*time.Minute),
60 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 20*time.Second),
61 HTTPClient: &http.Client{
62 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
63 },
64 }
65 }
66
67 // DNSProvider implements the challenge.Provider interface.
68 type DNSProvider struct {
69 config *Config
70 client *internal.Client
71
72 inProgressFQDNs map[string]inProgressInfo
73 inProgressMu sync.Mutex
74
75 // findZoneByFqdn determines the DNS zone of a FQDN.
76 // It is overridden during tests.
77 // only for testing purpose.
78 findZoneByFqdn func(fqdn string) (string, error)
79 }
80
81 // NewDNSProvider returns a DNSProvider instance configured for Gandi.
82 // Credentials must be passed in the environment variable: GANDIV5_API_KEY.
83 func NewDNSProvider() (*DNSProvider, error) {
84 // TODO(ldez): rewrite this when APIKey will be removed.
85 config := NewDefaultConfig()
86 config.APIKey = env.GetOrFile(EnvAPIKey)
87 config.PersonalAccessToken = env.GetOrFile(EnvPersonalAccessToken)
88
89 return NewDNSProviderConfig(config)
90 }
91
92 // NewDNSProviderConfig return a DNSProvider instance configured for Gandi.
93 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
94 if config == nil {
95 return nil, errors.New("gandiv5: the configuration of the DNS provider is nil")
96 }
97
98 if config.APIKey != "" {
99 log.Print("gandiv5: API Key is deprecated, use Personal Access Token instead")
100 }
101
102 if config.APIKey == "" && config.PersonalAccessToken == "" {
103 return nil, errors.New("gandiv5: credentials information are missing")
104 }
105
106 if config.TTL < minTTL {
107 return nil, fmt.Errorf("gandiv5: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
108 }
109
110 client := internal.NewClient(config.APIKey, config.PersonalAccessToken)
111
112 if config.BaseURL != "" {
113 baseURL, err := url.Parse(config.BaseURL)
114 if err != nil {
115 return nil, fmt.Errorf("gandiv5: %w", err)
116 }
117
118 client.BaseURL = baseURL
119 }
120
121 if config.HTTPClient != nil {
122 client.HTTPClient = config.HTTPClient
123 }
124
125 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
126
127 return &DNSProvider{
128 config: config,
129 client: client,
130 inProgressFQDNs: make(map[string]inProgressInfo),
131 findZoneByFqdn: dns01.FindZoneByFqdn,
132 }, nil
133 }
134
135 // Present creates a TXT record using the specified parameters.
136 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
137 info := dns01.GetChallengeInfo(domain, keyAuth)
138
139 // find authZone
140 authZone, err := d.findZoneByFqdn(info.EffectiveFQDN)
141 if err != nil {
142 return fmt.Errorf("gandiv5: could not find zone for domain %q: %w", domain, err)
143 }
144
145 // determine name of TXT record
146 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
147 if err != nil {
148 return fmt.Errorf("gandiv5: %w", err)
149 }
150
151 // acquire lock and check there is not a challenge already in
152 // progress for this value of authZone
153 d.inProgressMu.Lock()
154 defer d.inProgressMu.Unlock()
155
156 // add TXT record into authZone
157 err = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(authZone), subDomain, info.Value, d.config.TTL)
158 if err != nil {
159 return err
160 }
161
162 // save data necessary for CleanUp
163 d.inProgressFQDNs[info.EffectiveFQDN] = inProgressInfo{
164 authZone: authZone,
165 fieldName: subDomain,
166 }
167
168 return nil
169 }
170
171 // CleanUp removes the TXT record matching the specified parameters.
172 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
173 info := dns01.GetChallengeInfo(domain, keyAuth)
174
175 // acquire lock and retrieve authZone
176 d.inProgressMu.Lock()
177 defer d.inProgressMu.Unlock()
178
179 if _, ok := d.inProgressFQDNs[info.EffectiveFQDN]; !ok {
180 // if there is no cleanup information then just return
181 return nil
182 }
183
184 fieldName := d.inProgressFQDNs[info.EffectiveFQDN].fieldName
185 authZone := d.inProgressFQDNs[info.EffectiveFQDN].authZone
186 delete(d.inProgressFQDNs, info.EffectiveFQDN)
187
188 // delete TXT record from authZone
189 err := d.client.DeleteTXTRecord(context.Background(), dns01.UnFqdn(authZone), fieldName)
190 if err != nil {
191 return fmt.Errorf("gandiv5: %w", err)
192 }
193
194 return nil
195 }
196
197 // Timeout returns the timeout and interval to use when checking for DNS propagation.
198 // Adjusting here to cope with spikes in propagation times.
199 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
200 return d.config.PropagationTimeout, d.config.PollingInterval
201 }
202