cloudru.go raw
1 // Package cloudru implements a DNS provider for solving the DNS-01 challenge using cloud.ru DNS.
2 package cloudru
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "strconv"
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/cloudru/internal"
17 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
18 )
19
20 // Environment variables names.
21 const (
22 envNamespace = "CLOUDRU_"
23
24 EnvServiceInstanceID = envNamespace + "SERVICE_INSTANCE_ID"
25 EnvKeyID = envNamespace + "KEY_ID"
26 EnvSecret = envNamespace + "SECRET"
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 ServiceInstanceID string
40 KeyID string
41 Secret string
42
43 PropagationTimeout time.Duration
44 PollingInterval time.Duration
45 SequenceInterval time.Duration
46 HTTPClient *http.Client
47 TTL int
48 }
49
50 // NewDefaultConfig returns a default configuration for the DNSProvider.
51 func NewDefaultConfig() *Config {
52 return &Config{
53 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
54 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
55 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),
56 SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),
57 HTTPClient: &http.Client{
58 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
59 },
60 }
61 }
62
63 type DNSProvider struct {
64 config *Config
65 client *internal.Client
66
67 records map[string]*internal.Record
68 recordsMu sync.Mutex
69 }
70
71 // NewDNSProvider returns a DNSProvider instance configured for cloud.ru.
72 // Credentials must be passed in the environment variables:
73 // CLOUDRU_SERVICE_INSTANCE_ID, CLOUDRU_KEY_ID, and CLOUDRU_SECRET.
74 func NewDNSProvider() (*DNSProvider, error) {
75 values, err := env.Get(EnvServiceInstanceID, EnvKeyID, EnvSecret)
76 if err != nil {
77 return nil, fmt.Errorf("cloudru: %w", err)
78 }
79
80 config := NewDefaultConfig()
81 config.ServiceInstanceID = values[EnvServiceInstanceID]
82 config.KeyID = values[EnvKeyID]
83 config.Secret = values[EnvSecret]
84
85 return NewDNSProviderConfig(config)
86 }
87
88 // NewDNSProviderConfig return a DNSProvider instance configured for cloud.ru.
89 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
90 if config == nil {
91 return nil, errors.New("cloudru: the configuration of the DNS provider is nil")
92 }
93
94 if config.ServiceInstanceID == "" || config.KeyID == "" || config.Secret == "" {
95 return nil, errors.New("cloudru: some credentials information are missing")
96 }
97
98 client := internal.NewClient(config.KeyID, config.Secret)
99
100 if config.HTTPClient != nil {
101 client.HTTPClient = config.HTTPClient
102 }
103
104 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
105
106 return &DNSProvider{
107 config: config,
108 client: client,
109 records: make(map[string]*internal.Record),
110 }, nil
111 }
112
113 // Present creates a TXT record using the specified parameters.
114 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
115 info := dns01.GetChallengeInfo(domain, keyAuth)
116
117 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
118 if err != nil {
119 return fmt.Errorf("cloudru: could not find zone for domain %q: %w", domain, err)
120 }
121
122 authZone = dns01.UnFqdn(authZone)
123
124 ctx, err := d.client.CreateAuthenticatedContext(context.Background())
125 if err != nil {
126 return fmt.Errorf("cloudru: %w", err)
127 }
128
129 zone, err := d.getZoneInformationByName(ctx, d.config.ServiceInstanceID, authZone)
130 if err != nil {
131 return fmt.Errorf("cloudru: could not find zone information (ServiceInstanceID: %s, zone: %s): %w", d.config.ServiceInstanceID, authZone, err)
132 }
133
134 record := internal.Record{
135 Name: info.EffectiveFQDN,
136 Type: "TXT",
137 Values: []string{info.Value},
138 TTL: strconv.Itoa(d.config.TTL),
139 }
140
141 newRecord, err := d.client.CreateRecord(ctx, zone.ID, record)
142 if err != nil {
143 return fmt.Errorf("cloudru: could not create record: %w", err)
144 }
145
146 d.recordsMu.Lock()
147 d.records[token] = newRecord
148 d.recordsMu.Unlock()
149
150 return nil
151 }
152
153 // CleanUp removes a given record that was generated by Present.
154 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
155 info := dns01.GetChallengeInfo(domain, keyAuth)
156
157 d.recordsMu.Lock()
158 record, ok := d.records[token]
159 d.recordsMu.Unlock()
160
161 if !ok {
162 return fmt.Errorf("cloudru: unknown recordID for %q", info.EffectiveFQDN)
163 }
164
165 ctx, err := d.client.CreateAuthenticatedContext(context.Background())
166 if err != nil {
167 return fmt.Errorf("cloudru: %w", err)
168 }
169
170 err = d.client.DeleteRecord(ctx, record.ZoneID, record.Name, "TXT")
171 if err != nil {
172 return fmt.Errorf("cloudru: %w", err)
173 }
174
175 d.recordsMu.Lock()
176 delete(d.records, token)
177 d.recordsMu.Unlock()
178
179 return nil
180 }
181
182 // Sequential All DNS challenges for this provider will be resolved sequentially.
183 // Returns the interval between each iteration.
184 func (d *DNSProvider) Sequential() time.Duration {
185 return d.config.SequenceInterval
186 }
187
188 // Timeout returns the timeout and interval to use when checking for DNS propagation.
189 // Adjusting here to cope with spikes in propagation times.
190 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
191 return d.config.PropagationTimeout, d.config.PollingInterval
192 }
193
194 func (d *DNSProvider) getZoneInformationByName(ctx context.Context, parentID, name string) (internal.Zone, error) {
195 zs, err := d.client.GetZones(ctx, parentID)
196 if err != nil {
197 return internal.Zone{}, err
198 }
199
200 for _, element := range zs {
201 if element.Name == name {
202 return element, nil
203 }
204 }
205
206 return internal.Zone{}, errors.New("could not find Zone record")
207 }
208