nicru.go raw
1 // Package nicru implements a DNS provider for solving the DNS-01 challenge using RU Center.
2 package nicru
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "strconv"
9 "time"
10
11 "github.com/go-acme/lego/v4/challenge"
12 "github.com/go-acme/lego/v4/challenge/dns01"
13 "github.com/go-acme/lego/v4/platform/config/env"
14 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
15 "github.com/go-acme/lego/v4/providers/dns/nicru/internal"
16 )
17
18 // Environment variables names.
19 const (
20 envNamespace = "NICRU_"
21
22 EnvUsername = envNamespace + "USER"
23 EnvPassword = envNamespace + "PASSWORD"
24 EnvServiceID = envNamespace + "SERVICE_ID"
25 EnvSecret = envNamespace + "SECRET"
26
27 EnvTTL = envNamespace + "TTL"
28 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
29 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
30 )
31
32 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
33
34 // Config is used to configure the creation of the DNSProvider.
35 type Config struct {
36 TTL int
37 Username string
38 Password string
39 ServiceID string
40 Secret string
41 PropagationTimeout time.Duration
42 PollingInterval time.Duration
43 }
44
45 // NewDefaultConfig returns a default configuration for the DNSProvider.
46 func NewDefaultConfig() *Config {
47 return &Config{
48 TTL: env.GetOrDefaultInt(EnvTTL, 30),
49 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
50 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 1*time.Minute),
51 }
52 }
53
54 // DNSProvider implements the challenge.Provider interface.
55 type DNSProvider struct {
56 client *internal.Client
57 config *Config
58 }
59
60 // NewDNSProvider returns a DNSProvider instance configured for RU Center.
61 func NewDNSProvider() (*DNSProvider, error) {
62 values, err := env.Get(EnvUsername, EnvPassword, EnvServiceID, EnvSecret)
63 if err != nil {
64 return nil, fmt.Errorf("nicru: %w", err)
65 }
66
67 config := NewDefaultConfig()
68 config.Username = values[EnvUsername]
69 config.Password = values[EnvPassword]
70 config.ServiceID = values[EnvServiceID]
71 config.Secret = values[EnvSecret]
72
73 return NewDNSProviderConfig(config)
74 }
75
76 // NewDNSProviderConfig return a DNSProvider instance configured for RU Center.
77 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
78 if config == nil {
79 return nil, errors.New("nicru: the configuration of the DNS provider is nil")
80 }
81
82 clientCfg := &internal.OauthConfiguration{
83 OAuth2ClientID: config.ServiceID,
84 OAuth2SecretID: config.Secret,
85 Username: config.Username,
86 Password: config.Password,
87 }
88
89 oauthClient, err := internal.NewOauthClient(context.Background(), clientCfg)
90 if err != nil {
91 return nil, fmt.Errorf("nicru: %w", err)
92 }
93
94 client, err := internal.NewClient(clientdebug.Wrap(oauthClient))
95 if err != nil {
96 return nil, fmt.Errorf("nicru: unable to build API client: %w", err)
97 }
98
99 return &DNSProvider{
100 client: client,
101 config: config,
102 }, nil
103 }
104
105 // Present creates a TXT record to fulfill the dns-01 challenge.
106 func (d *DNSProvider) Present(domain, _, keyAuth string) error {
107 ctx := context.Background()
108
109 info := dns01.GetChallengeInfo(domain, keyAuth)
110
111 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
112 if err != nil {
113 return fmt.Errorf("nicru: could not find zone for domain %q: %w", domain, err)
114 }
115
116 authZone = dns01.UnFqdn(authZone)
117
118 zone, err := d.findZone(ctx, authZone)
119 if err != nil {
120 return fmt.Errorf("nicru: find zone: %w", err)
121 }
122
123 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
124 if err != nil {
125 return fmt.Errorf("nicru: %w", err)
126 }
127
128 records, err := d.client.GetRecords(ctx, zone.Service, authZone)
129 if err != nil {
130 return fmt.Errorf("nicru: get records: %w", err)
131 }
132
133 for _, record := range records {
134 if record.TXT == nil {
135 continue
136 }
137
138 if record.TXT.Text == subDomain && record.TXT.String == info.Value {
139 return nil
140 }
141 }
142
143 rrs := []internal.RR{{
144 Name: subDomain,
145 TTL: strconv.Itoa(d.config.TTL),
146 Type: "TXT",
147 TXT: &internal.TXT{String: info.Value},
148 }}
149
150 _, err = d.client.AddRecords(ctx, zone.Service, authZone, rrs)
151 if err != nil {
152 return fmt.Errorf("nicru: add records: %w", err)
153 }
154
155 err = d.client.CommitZone(ctx, zone.Service, authZone)
156 if err != nil {
157 return fmt.Errorf("nicru: commit zone: %w", err)
158 }
159
160 return nil
161 }
162
163 // CleanUp removes the TXT record matching the specified parameters.
164 func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {
165 ctx := context.Background()
166
167 info := dns01.GetChallengeInfo(domain, keyAuth)
168
169 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
170 if err != nil {
171 return fmt.Errorf("nicru: could not find zone for domain %q: %w", domain, err)
172 }
173
174 authZone = dns01.UnFqdn(authZone)
175
176 zone, err := d.findZone(ctx, authZone)
177 if err != nil {
178 return fmt.Errorf("nicru: find zone: %w", err)
179 }
180
181 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
182 if err != nil {
183 return fmt.Errorf("nicru: %w", err)
184 }
185
186 records, err := d.client.GetRecords(ctx, zone.Service, authZone)
187 if err != nil {
188 return fmt.Errorf("nicru: get records: %w", err)
189 }
190
191 subDomain = dns01.UnFqdn(subDomain)
192
193 for _, record := range records {
194 if record.TXT == nil {
195 continue
196 }
197
198 if record.Name != subDomain || record.TXT.String != info.Value {
199 continue
200 }
201
202 err = d.client.DeleteRecord(ctx, zone.Service, authZone, record.ID)
203 if err != nil {
204 return fmt.Errorf("nicru: delete record: %w", err)
205 }
206 }
207
208 err = d.client.CommitZone(ctx, zone.Service, authZone)
209 if err != nil {
210 return fmt.Errorf("nicru: commit zone: %w", err)
211 }
212
213 return nil
214 }
215
216 // Timeout returns the timeout and interval to use when checking for DNS propagation.
217 // Adjusting here to cope with spikes in propagation times.
218 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
219 return d.config.PropagationTimeout, d.config.PollingInterval
220 }
221
222 func (d *DNSProvider) findZone(ctx context.Context, authZone string) (*internal.Zone, error) {
223 zones, err := d.client.ListZones(ctx)
224 if err != nil {
225 return nil, fmt.Errorf("unable to fetch dns zones: %w", err)
226 }
227
228 if len(zones) == 0 {
229 return nil, errors.New("no zones found")
230 }
231
232 for _, zone := range zones {
233 if zone.Name == authZone {
234 return &zone, nil
235 }
236 }
237
238 return nil, fmt.Errorf("zone not found for %s", authZone)
239 }
240