luadns.go raw
1 // Package luadns implements a DNS provider for solving the DNS-01 challenge using LuaDNS.
2 package luadns
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "strings"
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/internal/clientdebug"
17 "github.com/go-acme/lego/v4/providers/dns/luadns/internal"
18 )
19
20 // Environment variables names.
21 const (
22 envNamespace = "LUADNS_"
23
24 EnvAPIUsername = envNamespace + "API_USERNAME"
25 EnvAPIToken = envNamespace + "API_TOKEN"
26
27 EnvTTL = envNamespace + "TTL"
28 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
29 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
30 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
31 )
32
33 const minTTL = 300
34
35 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
36
37 // Config is used to configure the creation of the DNSProvider.
38 type Config struct {
39 APIUsername string
40 APIToken string
41 PropagationTimeout time.Duration
42 PollingInterval time.Duration
43 TTL int
44 HTTPClient *http.Client
45 }
46
47 // NewDefaultConfig returns a default configuration for the DNSProvider.
48 func NewDefaultConfig() *Config {
49 return &Config{
50 TTL: env.GetOrDefaultInt(EnvTTL, minTTL),
51 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),
52 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
53 HTTPClient: &http.Client{
54 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
55 },
56 }
57 }
58
59 // DNSProvider implements the challenge.Provider interface.
60 type DNSProvider struct {
61 config *Config
62 client *internal.Client
63
64 recordsMu sync.Mutex
65 records map[string]*internal.DNSRecord
66 }
67
68 // NewDNSProvider returns a DNSProvider instance configured for LuaDNS.
69 // Credentials must be passed in the environment variables:
70 // LUADNS_API_USERNAME and LUADNS_API_TOKEN.
71 func NewDNSProvider() (*DNSProvider, error) {
72 values, err := env.Get(EnvAPIUsername, EnvAPIToken)
73 if err != nil {
74 return nil, fmt.Errorf("luadns: %w", err)
75 }
76
77 config := NewDefaultConfig()
78 config.APIUsername = values[EnvAPIUsername]
79 config.APIToken = values[EnvAPIToken]
80
81 return NewDNSProviderConfig(config)
82 }
83
84 // NewDNSProviderConfig return a DNSProvider instance configured for LuaDNS.
85 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
86 if config == nil {
87 return nil, errors.New("luadns: the configuration of the DNS provider is nil")
88 }
89
90 if config.APIUsername == "" || config.APIToken == "" {
91 return nil, errors.New("luadns: credentials missing")
92 }
93
94 if config.TTL < minTTL {
95 return nil, fmt.Errorf("luadns: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
96 }
97
98 client := internal.NewClient(config.APIUsername, config.APIToken)
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.DNSRecord),
110 }, nil
111 }
112
113 // Timeout returns the timeout and interval to use when checking for DNS propagation.
114 // Adjusting here to cope with spikes in propagation times.
115 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
116 return d.config.PropagationTimeout, d.config.PollingInterval
117 }
118
119 // Present creates a TXT record using the specified parameters.
120 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
121 info := dns01.GetChallengeInfo(domain, keyAuth)
122
123 ctx := context.Background()
124
125 zones, err := d.client.ListZones(ctx)
126 if err != nil {
127 return fmt.Errorf("luadns: failed to get zones: %w", err)
128 }
129
130 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
131 if err != nil {
132 return fmt.Errorf("luadns: could not find zone for domain %q: %w", domain, err)
133 }
134
135 zone := findZone(zones, dns01.UnFqdn(authZone))
136 if zone == nil {
137 return fmt.Errorf("luadns: no matching zone found for domain %s", domain)
138 }
139
140 newRecord := internal.DNSRecord{
141 Name: info.EffectiveFQDN,
142 Type: "TXT",
143 Content: info.Value,
144 TTL: d.config.TTL,
145 }
146
147 record, err := d.client.CreateRecord(ctx, *zone, newRecord)
148 if err != nil {
149 return fmt.Errorf("luadns: failed to create record: %w", err)
150 }
151
152 d.recordsMu.Lock()
153 d.records[token] = record
154 d.recordsMu.Unlock()
155
156 return nil
157 }
158
159 // CleanUp removes the TXT record matching the specified parameters.
160 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
161 info := dns01.GetChallengeInfo(domain, keyAuth)
162
163 d.recordsMu.Lock()
164 record, ok := d.records[token]
165 d.recordsMu.Unlock()
166
167 if !ok {
168 return fmt.Errorf("luadns: unknown record ID for '%s'", info.EffectiveFQDN)
169 }
170
171 err := d.client.DeleteRecord(context.Background(), record)
172 if err != nil {
173 return fmt.Errorf("luadns: failed to delete record: %w", err)
174 }
175
176 // Delete record from map
177 d.recordsMu.Lock()
178 delete(d.records, token)
179 d.recordsMu.Unlock()
180
181 return nil
182 }
183
184 func findZone(zones []internal.DNSZone, domain string) *internal.DNSZone {
185 var result *internal.DNSZone
186
187 for _, zone := range zones {
188 if zone.Name != "" && strings.HasSuffix(domain, zone.Name) {
189 if result == nil || len(zone.Name) > len(result.Name) {
190 result = &zone
191 }
192 }
193 }
194
195 return result
196 }
197