provider.go raw
1 // Package selectel implements a DNS provider for solving the DNS-01 challenge using Selectel Domains API.
2 package selectel
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "net/url"
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/providers/dns/internal/clientdebug"
15 "github.com/go-acme/lego/v4/providers/dns/internal/selectel/internal"
16 )
17
18 const MinTTL = 60
19
20 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
21
22 // Config is used to configure the creation of the DNSProvider.
23 type Config struct {
24 Token string
25 PropagationTimeout time.Duration
26 PollingInterval time.Duration
27 TTL int
28 HTTPClient *http.Client
29
30 // TODO(ldez): remove in v5?
31 BaseURL string
32 }
33
34 // DNSProvider implements the challenge.Provider interface.
35 type DNSProvider struct {
36 config *Config
37 client *internal.Client
38 }
39
40 // NewDNSProviderConfig return a DNSProvider instance configured for selectel.
41 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
42 if config == nil {
43 return nil, errors.New("the configuration of the DNS provider is nil")
44 }
45
46 if config.Token == "" {
47 return nil, errors.New("credentials missing")
48 }
49
50 if config.TTL < MinTTL {
51 return nil, fmt.Errorf("invalid TTL, TTL (%d) must be greater than %d", config.TTL, MinTTL)
52 }
53
54 client := internal.NewClient(config.Token)
55
56 if config.HTTPClient != nil {
57 client.HTTPClient = config.HTTPClient
58 }
59
60 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
61
62 var err error
63
64 client.BaseURL, err = url.Parse(config.BaseURL)
65 if err != nil {
66 return nil, fmt.Errorf("%w", err)
67 }
68
69 return &DNSProvider{config: config, client: client}, nil
70 }
71
72 // Timeout returns the Timeout and interval to use when checking for DNS propagation.
73 // Adjusting here to cope with spikes in propagation times.
74 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
75 return d.config.PropagationTimeout, d.config.PollingInterval
76 }
77
78 // Present creates a TXT record to fulfill DNS-01 challenge.
79 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
80 info := dns01.GetChallengeInfo(domain, keyAuth)
81
82 ctx := context.Background()
83
84 // TODO(ldez) replace domain by FQDN to follow CNAME.
85 domainObj, err := d.client.GetDomainByName(ctx, domain)
86 if err != nil {
87 return fmt.Errorf("get domain by name: %w", err)
88 }
89
90 txtRecord := internal.Record{
91 Type: "TXT",
92 TTL: d.config.TTL,
93 Name: info.EffectiveFQDN,
94 Content: info.Value,
95 }
96
97 _, err = d.client.AddRecord(ctx, domainObj.ID, txtRecord)
98 if err != nil {
99 return fmt.Errorf("add record: %w", err)
100 }
101
102 return nil
103 }
104
105 // CleanUp removes a TXT record used for DNS-01 challenge.
106 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
107 info := dns01.GetChallengeInfo(domain, keyAuth)
108
109 recordName := dns01.UnFqdn(info.EffectiveFQDN)
110
111 ctx := context.Background()
112
113 // TODO(ldez) replace domain by FQDN to follow CNAME.
114 domainObj, err := d.client.GetDomainByName(ctx, domain)
115 if err != nil {
116 return fmt.Errorf("%w", err)
117 }
118
119 records, err := d.client.ListRecords(ctx, domainObj.ID)
120 if err != nil {
121 return fmt.Errorf("list records: %w", err)
122 }
123
124 // Delete records with specific FQDN
125 var lastErr error
126
127 for _, record := range records {
128 if record.Name == recordName {
129 err = d.client.DeleteRecord(ctx, domainObj.ID, record.ID)
130 if err != nil {
131 lastErr = fmt.Errorf("delete record: %w", err)
132 }
133 }
134 }
135
136 return lastErr
137 }
138