versio.go raw
1 // Package versio implements a DNS provider for solving the DNS-01 challenge using versio DNS.
2 package versio
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/platform/config/env"
16 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
17 "github.com/go-acme/lego/v4/providers/dns/versio/internal"
18 )
19
20 // Environment variables names.
21 const (
22 envNamespace = "VERSIO_"
23
24 EnvUsername = envNamespace + "USERNAME"
25 EnvPassword = envNamespace + "PASSWORD"
26 EnvEndpoint = envNamespace + "ENDPOINT"
27
28 EnvTTL = envNamespace + "TTL"
29 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
30 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
31 EnvSequenceInterval = envNamespace + "SEQUENCE_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 *url.URL
40 TTL int
41 Username string
42 Password string
43 PropagationTimeout time.Duration
44 PollingInterval time.Duration
45 SequenceInterval time.Duration
46 HTTPClient *http.Client
47 }
48
49 // NewDefaultConfig returns a default configuration for the DNSProvider.
50 func NewDefaultConfig() *Config {
51 baseURL, err := url.Parse(env.GetOrDefaultString(EnvEndpoint, internal.DefaultBaseURL))
52 if err != nil {
53 baseURL, _ = url.Parse(internal.DefaultBaseURL)
54 }
55
56 return &Config{
57 BaseURL: baseURL,
58 TTL: env.GetOrDefaultInt(EnvTTL, 300),
59 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
60 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),
61 SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),
62 HTTPClient: &http.Client{
63 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
64 },
65 }
66 }
67
68 // DNSProvider implements the challenge.Provider interface.
69 type DNSProvider struct {
70 config *Config
71 client *internal.Client
72
73 dnsEntriesMu sync.Mutex
74 }
75
76 // NewDNSProvider returns a DNSProvider instance.
77 func NewDNSProvider() (*DNSProvider, error) {
78 values, err := env.Get(EnvUsername, EnvPassword)
79 if err != nil {
80 return nil, fmt.Errorf("versio: %w", err)
81 }
82
83 config := NewDefaultConfig()
84 config.Username = values[EnvUsername]
85 config.Password = values[EnvPassword]
86
87 return NewDNSProviderConfig(config)
88 }
89
90 // NewDNSProviderConfig return a DNSProvider instance configured for Versio.
91 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
92 if config == nil {
93 return nil, errors.New("versio: the configuration of the DNS provider is nil")
94 }
95
96 if config.Username == "" {
97 return nil, errors.New("versio: the versio username is missing")
98 }
99
100 if config.Password == "" {
101 return nil, errors.New("versio: the versio password is missing")
102 }
103
104 client := internal.NewClient(config.Username, config.Password)
105
106 if config.BaseURL != nil {
107 client.BaseURL = config.BaseURL
108 }
109
110 if config.HTTPClient != nil {
111 client.HTTPClient = config.HTTPClient
112 }
113
114 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
115
116 return &DNSProvider{config: config, client: client}, nil
117 }
118
119 // Timeout returns the timeout and interval to use when checking for DNS propagation.
120 // Adjusting here to cope with spikes in propagation times.
121 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
122 return d.config.PropagationTimeout, d.config.PollingInterval
123 }
124
125 // Present creates a TXT record to fulfill the dns-01 challenge.
126 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
127 info := dns01.GetChallengeInfo(domain, keyAuth)
128
129 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
130 if err != nil {
131 return fmt.Errorf("versio: could not find zone for domain %q: %w", domain, err)
132 }
133
134 // use mutex to prevent race condition from getDNSRecords until postDNSRecords
135 d.dnsEntriesMu.Lock()
136 defer d.dnsEntriesMu.Unlock()
137
138 ctx := context.Background()
139
140 zoneName := dns01.UnFqdn(authZone)
141
142 domains, err := d.client.GetDomain(ctx, zoneName)
143 if err != nil {
144 return fmt.Errorf("versio: %w", err)
145 }
146
147 txtRecord := internal.Record{
148 Type: "TXT",
149 Name: info.EffectiveFQDN,
150 Value: `"` + info.Value + `"`,
151 TTL: d.config.TTL,
152 }
153
154 // Add new txtRecord to existing array of DNSRecords.
155 // We'll need all the dns_records to add a new TXT record.
156 msg := &domains.DomainInfo
157 msg.DNSRecords = append(msg.DNSRecords, txtRecord)
158
159 _, err = d.client.UpdateDomain(ctx, zoneName, msg)
160 if err != nil {
161 return fmt.Errorf("versio: %w", err)
162 }
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 info := dns01.GetChallengeInfo(domain, keyAuth)
170
171 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
172 if err != nil {
173 return fmt.Errorf("versio: could not find zone for domain %q: %w", domain, err)
174 }
175
176 // use mutex to prevent race condition from getDNSRecords until postDNSRecords
177 d.dnsEntriesMu.Lock()
178 defer d.dnsEntriesMu.Unlock()
179
180 ctx := context.Background()
181
182 zoneName := dns01.UnFqdn(authZone)
183
184 domains, err := d.client.GetDomain(ctx, zoneName)
185 if err != nil {
186 return fmt.Errorf("versio: %w", err)
187 }
188
189 // loop through the existing entries and remove the specific record
190 msg := &internal.DomainInfo{}
191
192 for _, e := range domains.DomainInfo.DNSRecords {
193 if e.Name != info.EffectiveFQDN {
194 msg.DNSRecords = append(msg.DNSRecords, e)
195 }
196 }
197
198 _, err = d.client.UpdateDomain(ctx, zoneName, msg)
199 if err != nil {
200 return fmt.Errorf("versio: %w", err)
201 }
202
203 return nil
204 }
205