nifcloud.go raw
1 // Package nifcloud implements a DNS provider for solving the DNS-01 challenge using NIFCLOUD DNS.
2 package nifcloud
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "net/url"
10 "time"
11
12 "github.com/cenkalti/backoff/v5"
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/platform/wait"
17 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
18 "github.com/go-acme/lego/v4/providers/dns/nifcloud/internal"
19 )
20
21 // Environment variables names.
22 const (
23 envNamespace = "NIFCLOUD_"
24
25 EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID"
26 EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY"
27 EnvDNSEndpoint = envNamespace + "DNS_ENDPOINT"
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 BaseURL string
40 AccessKey string
41 SecretKey string
42 PropagationTimeout time.Duration
43 PollingInterval time.Duration
44 TTL int
45 HTTPClient *http.Client
46 }
47
48 // NewDefaultConfig returns a default configuration for the DNSProvider.
49 func NewDefaultConfig() *Config {
50 return &Config{
51 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
52 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
53 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
54 HTTPClient: &http.Client{
55 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
56 },
57 }
58 }
59
60 // DNSProvider implements the challenge.Provider interface.
61 type DNSProvider struct {
62 client *internal.Client
63 config *Config
64 }
65
66 // NewDNSProvider returns a DNSProvider instance configured for the NIFCLOUD DNS service.
67 // Credentials must be passed in the environment variables:
68 // NIFCLOUD_ACCESS_KEY_ID and NIFCLOUD_SECRET_ACCESS_KEY.
69 func NewDNSProvider() (*DNSProvider, error) {
70 values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey)
71 if err != nil {
72 return nil, fmt.Errorf("nifcloud: %w", err)
73 }
74
75 config := NewDefaultConfig()
76 config.BaseURL = env.GetOrFile(EnvDNSEndpoint)
77 config.AccessKey = values[EnvAccessKeyID]
78 config.SecretKey = values[EnvSecretAccessKey]
79
80 return NewDNSProviderConfig(config)
81 }
82
83 // NewDNSProviderConfig return a DNSProvider instance configured for NIFCLOUD.
84 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
85 if config == nil {
86 return nil, errors.New("nifcloud: the configuration of the DNS provider is nil")
87 }
88
89 client, err := internal.NewClient(config.AccessKey, config.SecretKey)
90 if err != nil {
91 return nil, fmt.Errorf("nifcloud: %w", err)
92 }
93
94 if config.HTTPClient != nil {
95 client.HTTPClient = config.HTTPClient
96 }
97
98 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
99
100 if config.BaseURL != "" {
101 baseURL, err := url.Parse(config.BaseURL)
102 if err != nil {
103 return nil, fmt.Errorf("nifcloud: %w", err)
104 }
105
106 client.BaseURL = baseURL
107 }
108
109 return &DNSProvider{client: client, config: config}, nil
110 }
111
112 // Present creates a TXT record using the specified parameters.
113 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
114 ctx := context.Background()
115
116 info := dns01.GetChallengeInfo(domain, keyAuth)
117
118 err := d.changeRecord(ctx, "CREATE", info.EffectiveFQDN, info.Value, d.config.TTL)
119 if err != nil {
120 return fmt.Errorf("nifcloud: %w", err)
121 }
122
123 return err
124 }
125
126 // CleanUp removes the TXT record matching the specified parameters.
127 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
128 ctx := context.Background()
129
130 info := dns01.GetChallengeInfo(domain, keyAuth)
131
132 err := d.changeRecord(ctx, "DELETE", info.EffectiveFQDN, info.Value, d.config.TTL)
133 if err != nil {
134 return fmt.Errorf("nifcloud: %w", err)
135 }
136
137 return err
138 }
139
140 // Timeout returns the timeout and interval to use when checking for DNS propagation.
141 // Adjusting here to cope with spikes in propagation times.
142 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
143 return d.config.PropagationTimeout, d.config.PollingInterval
144 }
145
146 func (d *DNSProvider) changeRecord(ctx context.Context, action, fqdn, value string, ttl int) error {
147 authZone, err := dns01.FindZoneByFqdn(fqdn)
148 if err != nil {
149 return fmt.Errorf("could not find zone: %w", err)
150 }
151
152 name := dns01.UnFqdn(fqdn)
153 if authZone == fqdn {
154 name = "@"
155 }
156
157 reqParams := internal.ChangeResourceRecordSetsRequest{
158 XMLNs: internal.XMLNs,
159 ChangeBatch: internal.ChangeBatch{
160 Comment: "Managed by Lego",
161 Changes: internal.Changes{
162 Change: []internal.Change{
163 {
164 Action: action,
165 ResourceRecordSet: internal.ResourceRecordSet{
166 Name: name,
167 Type: "TXT",
168 TTL: ttl,
169 ResourceRecords: internal.ResourceRecords{
170 ResourceRecord: []internal.ResourceRecord{
171 {
172 Value: value,
173 },
174 },
175 },
176 },
177 },
178 },
179 },
180 },
181 }
182
183 resp, err := d.client.ChangeResourceRecordSets(ctx, dns01.UnFqdn(authZone), reqParams)
184 if err != nil {
185 return fmt.Errorf("failed to change record set: %w", err)
186 }
187
188 statusID := resp.ChangeInfo.ID
189
190 return wait.Retry(ctx,
191 func() error {
192 resp, err := d.client.GetChange(ctx, statusID)
193 if err != nil {
194 return fmt.Errorf("get change: %w", err)
195 }
196
197 if resp.ChangeInfo.Status != "INSYNC" {
198 return fmt.Errorf("change status: %s", resp.ChangeInfo.Status)
199 }
200
201 return nil
202 },
203 backoff.WithBackOff(backoff.NewConstantBackOff(4*time.Second)),
204 backoff.WithMaxElapsedTime(120*time.Second),
205 )
206 }
207