vinyldns.go raw
1 // Package vinyldns implements a DNS provider for solving the DNS-01 challenge using VinylDNS.
2 package vinyldns
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "strconv"
10 "time"
11
12 "github.com/go-acme/lego/v4/challenge"
13 "github.com/go-acme/lego/v4/challenge/dns01"
14 "github.com/go-acme/lego/v4/platform/config/env"
15 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
16 "github.com/go-acme/lego/v4/providers/dns/internal/useragent"
17 "github.com/vinyldns/go-vinyldns/vinyldns"
18 )
19
20 // Environment variables names.
21 const (
22 envNamespace = "VINYLDNS_"
23
24 EnvAccessKey = envNamespace + "ACCESS_KEY"
25 EnvSecretKey = envNamespace + "SECRET_KEY"
26 EnvHost = envNamespace + "HOST"
27 EnvQuoteValue = envNamespace + "QUOTE_VALUE"
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 AccessKey string
40 SecretKey string
41 Host string
42 QuoteValue bool
43
44 TTL int
45 PropagationTimeout time.Duration
46 PollingInterval time.Duration
47 HTTPClient *http.Client
48 }
49
50 // NewDefaultConfig returns a default configuration for the DNSProvider.
51 func NewDefaultConfig() *Config {
52 return &Config{
53 TTL: env.GetOrDefaultInt(EnvTTL, 30),
54 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
55 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second),
56 HTTPClient: &http.Client{
57 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
58 },
59 }
60 }
61
62 // DNSProvider implements the challenge.Provider interface.
63 type DNSProvider struct {
64 client *vinyldns.Client
65 config *Config
66 }
67
68 // NewDNSProvider returns a DNSProvider instance configured for VinylDNS.
69 // Credentials must be passed in the environment variables:
70 // VINYLDNS_ACCESS_KEY, VINYLDNS_SECRET_KEY, VINYLDNS_HOST.
71 func NewDNSProvider() (*DNSProvider, error) {
72 values, err := env.Get(EnvAccessKey, EnvSecretKey, EnvHost)
73 if err != nil {
74 return nil, fmt.Errorf("vinyldns: %w", err)
75 }
76
77 config := NewDefaultConfig()
78 config.AccessKey = values[EnvAccessKey]
79 config.SecretKey = values[EnvSecretKey]
80 config.Host = values[EnvHost]
81 config.QuoteValue = env.GetOrDefaultBool(EnvQuoteValue, false)
82
83 return NewDNSProviderConfig(config)
84 }
85
86 // NewDNSProviderConfig return a DNSProvider instance configured for VinylDNS.
87 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
88 if config == nil {
89 return nil, errors.New("vinyldns: the configuration of the VinylDNS DNS provider is nil")
90 }
91
92 if config.AccessKey == "" || config.SecretKey == "" {
93 return nil, errors.New("vinyldns: credentials are missing")
94 }
95
96 if config.Host == "" {
97 return nil, errors.New("vinyldns: host is missing")
98 }
99
100 client := vinyldns.NewClient(vinyldns.ClientConfiguration{
101 AccessKey: config.AccessKey,
102 SecretKey: config.SecretKey,
103 Host: config.Host,
104 UserAgent: useragent.Get(),
105 })
106
107 if config.HTTPClient != nil {
108 client.HTTPClient = config.HTTPClient
109 } else {
110 // For compatibility, it should be removed in v5.
111 client.HTTPClient.Timeout = 30 * time.Second
112 }
113
114 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
115
116 return &DNSProvider{client: client, config: config}, nil
117 }
118
119 // Present creates a TXT record to fulfill the dns-01 challenge.
120 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
121 ctx := context.Background()
122
123 info := dns01.GetChallengeInfo(domain, keyAuth)
124
125 existingRecord, err := d.getRecordSet(info.EffectiveFQDN)
126 if err != nil {
127 return fmt.Errorf("vinyldns: %w", err)
128 }
129
130 value := d.formatValue(info.Value)
131
132 record := vinyldns.Record{Text: value}
133
134 if existingRecord == nil || existingRecord.ID == "" {
135 err = d.createRecordSet(ctx, info.EffectiveFQDN, []vinyldns.Record{record})
136 if err != nil {
137 return fmt.Errorf("vinyldns: %w", err)
138 }
139
140 return nil
141 }
142
143 for _, i := range existingRecord.Records {
144 if i.Text == value {
145 return nil
146 }
147 }
148
149 records := existingRecord.Records
150 records = append(records, record)
151
152 err = d.updateRecordSet(ctx, existingRecord, records)
153 if err != nil {
154 return fmt.Errorf("vinyldns: %w", err)
155 }
156
157 return nil
158 }
159
160 // CleanUp removes the TXT record matching the specified parameters.
161 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
162 ctx := context.Background()
163
164 info := dns01.GetChallengeInfo(domain, keyAuth)
165
166 existingRecord, err := d.getRecordSet(info.EffectiveFQDN)
167 if err != nil {
168 return fmt.Errorf("vinyldns: %w", err)
169 }
170
171 if existingRecord == nil || existingRecord.ID == "" || len(existingRecord.Records) == 0 {
172 return nil
173 }
174
175 value := d.formatValue(info.Value)
176
177 var records []vinyldns.Record
178
179 for _, i := range existingRecord.Records {
180 if i.Text != value {
181 records = append(records, i)
182 }
183 }
184
185 if len(records) == 0 {
186 err = d.deleteRecordSet(ctx, existingRecord)
187 if err != nil {
188 return fmt.Errorf("vinyldns: %w", err)
189 }
190
191 return nil
192 }
193
194 err = d.updateRecordSet(ctx, existingRecord, records)
195 if err != nil {
196 return fmt.Errorf("vinyldns: %w", err)
197 }
198
199 return nil
200 }
201
202 // Timeout returns the timeout and interval to use when checking for DNS propagation.
203 // Adjusting here to cope with spikes in propagation times.
204 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
205 return d.config.PropagationTimeout, d.config.PollingInterval
206 }
207
208 func (d *DNSProvider) formatValue(v string) string {
209 if d.config.QuoteValue {
210 return strconv.Quote(v)
211 }
212
213 return v
214 }
215