nicmanager.go raw
1 // Package nicmanager implements a DNS provider for solving the DNS-01 challenge using nicmanager DNS.
2 package nicmanager
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "strings"
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/nicmanager/internal"
17 )
18
19 // Environment variables names.
20 const (
21 envNamespace = "NICMANAGER_"
22
23 EnvLogin = envNamespace + "API_LOGIN"
24 EnvUsername = envNamespace + "API_USERNAME"
25 EnvEmail = envNamespace + "API_EMAIL"
26 EnvPassword = envNamespace + "API_PASSWORD"
27 EnvOTP = envNamespace + "API_OTP"
28 EnvMode = envNamespace + "API_MODE"
29
30 EnvTTL = envNamespace + "TTL"
31 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
32 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
33 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
34 )
35
36 const minTTL = 900
37
38 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
39
40 // Config is used to configure the creation of the DNSProvider.
41 type Config struct {
42 Login string
43 Username string
44 Email string
45 Password string
46 OTPSecret string
47 Mode string
48
49 PropagationTimeout time.Duration
50 PollingInterval time.Duration
51 TTL int
52 HTTPClient *http.Client
53 }
54
55 // NewDefaultConfig returns a default configuration for the DNSProvider.
56 func NewDefaultConfig() *Config {
57 return &Config{
58 TTL: env.GetOrDefaultInt(EnvTTL, minTTL),
59 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
60 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
61 HTTPClient: &http.Client{
62 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
63 },
64 }
65 }
66
67 // DNSProvider implements the challenge.Provider interface.
68 type DNSProvider struct {
69 client *internal.Client
70 config *Config
71 }
72
73 // NewDNSProvider returns a DNSProvider instance configured for nicmanager.
74 // Credentials must be passed in the environment variables:
75 // NICMANAGER_API_LOGIN, NICMANAGER_API_USERNAME
76 // NICMANAGER_API_EMAIL
77 // NICMANAGER_API_PASSWORD
78 // NICMANAGER_API_OTP
79 // NICMANAGER_API_MODE.
80 func NewDNSProvider() (*DNSProvider, error) {
81 values, err := env.Get(EnvPassword)
82 if err != nil {
83 return nil, fmt.Errorf("nicmanager: %w", err)
84 }
85
86 config := NewDefaultConfig()
87 config.Password = values[EnvPassword]
88
89 config.Mode = env.GetOneWithFallback(EnvMode, internal.ModeAnycast, env.ParseString, envNamespace+"MODE")
90 config.Username = env.GetOrFile(EnvUsername)
91 config.Login = env.GetOrFile(EnvLogin)
92 config.Email = env.GetOrFile(EnvEmail)
93 config.OTPSecret = env.GetOrFile(EnvOTP)
94
95 if config.TTL < minTTL {
96 return nil, fmt.Errorf("TTL must be higher than %d: %d", minTTL, config.TTL)
97 }
98
99 return NewDNSProviderConfig(config)
100 }
101
102 // NewDNSProviderConfig return a DNSProvider instance configured for nicmanager.
103 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
104 if config == nil {
105 return nil, errors.New("nicmanager: the configuration of the DNS provider is nil")
106 }
107
108 opts := internal.Options{
109 Password: config.Password,
110 OTP: config.OTPSecret,
111 Mode: config.Mode,
112 }
113
114 switch {
115 case config.Password == "":
116 return nil, errors.New("nicmanager: credentials missing")
117 case config.Email != "":
118 opts.Email = config.Email
119 case config.Login != "" && config.Username != "":
120 opts.Login = config.Login
121 opts.Username = config.Username
122 default:
123 return nil, errors.New("nicmanager: credentials missing")
124 }
125
126 client := internal.NewClient(opts)
127
128 if config.HTTPClient != nil {
129 client.HTTPClient = config.HTTPClient
130 }
131
132 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
133
134 return &DNSProvider{client: client, config: config}, nil
135 }
136
137 // Timeout returns the timeout and interval to use when checking for DNS propagation.
138 // Adjusting here to cope with spikes in propagation times.
139 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
140 return d.config.PropagationTimeout, d.config.PollingInterval
141 }
142
143 // Present creates a TXT record to fulfill the dns-01 challenge.
144 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
145 info := dns01.GetChallengeInfo(domain, keyAuth)
146
147 rootDomain, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
148 if err != nil {
149 return fmt.Errorf("nicmanager: could not find zone for domain %q: %w", domain, err)
150 }
151
152 ctx := context.Background()
153
154 zone, err := d.client.GetZone(ctx, dns01.UnFqdn(rootDomain))
155 if err != nil {
156 return fmt.Errorf("nicmanager: failed to get zone %q: %w", rootDomain, err)
157 }
158
159 // The way nic manager deals with record with multiple values is that they are completely different records with unique ids
160 // Hence we don't check for an existing record here, but rather just create one
161 record := internal.RecordCreateUpdate{
162 Name: info.EffectiveFQDN,
163 Type: "TXT",
164 TTL: d.config.TTL,
165 Value: info.Value,
166 }
167
168 err = d.client.AddRecord(ctx, zone.Name, record)
169 if err != nil {
170 return fmt.Errorf("nicmanager: failed to create record [zone: %q, fqdn: %q]: %w", zone.Name, info.EffectiveFQDN, err)
171 }
172
173 return nil
174 }
175
176 // CleanUp removes the TXT record matching the specified parameters.
177 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
178 info := dns01.GetChallengeInfo(domain, keyAuth)
179
180 rootDomain, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
181 if err != nil {
182 return fmt.Errorf("nicmanager: could not find zone for domain %q: %w", domain, err)
183 }
184
185 ctx := context.Background()
186
187 zone, err := d.client.GetZone(ctx, dns01.UnFqdn(rootDomain))
188 if err != nil {
189 return fmt.Errorf("nicmanager: failed to get zone %q: %w", rootDomain, err)
190 }
191
192 name := dns01.UnFqdn(info.EffectiveFQDN)
193
194 var (
195 existingRecord internal.Record
196 existingRecordFound bool
197 )
198
199 for _, record := range zone.Records {
200 if strings.EqualFold(record.Type, "TXT") && strings.EqualFold(record.Name, name) && record.Content == info.Value {
201 existingRecord = record
202 existingRecordFound = true
203 }
204 }
205
206 if existingRecordFound {
207 err = d.client.DeleteRecord(ctx, zone.Name, existingRecord.ID)
208 if err != nil {
209 return fmt.Errorf("nicmanager: failed to delete record [zone: %q, domain: %q]: %w", zone.Name, name, err)
210 }
211 }
212
213 return errors.New("nicmanager: no record found to clean up")
214 }
215