pdns.go raw
1 // Package pdns implements a DNS provider for solving the DNS-01 challenge using PowerDNS nameserver.
2 package pdns
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "net/url"
10 "strconv"
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/log"
16 "github.com/go-acme/lego/v4/platform/config/env"
17 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
18 "github.com/go-acme/lego/v4/providers/dns/pdns/internal"
19 )
20
21 // Environment variables names.
22 const (
23 envNamespace = "PDNS_"
24
25 EnvAPIKey = envNamespace + "API_KEY"
26 EnvAPIURL = envNamespace + "API_URL"
27
28 EnvTTL = envNamespace + "TTL"
29 EnvAPIVersion = envNamespace + "API_VERSION"
30 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
31 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
32 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
33 EnvServerName = envNamespace + "SERVER_NAME"
34 )
35
36 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
37
38 // Config is used to configure the creation of the DNSProvider.
39 type Config struct {
40 APIKey string
41 Host *url.URL
42 ServerName string
43 APIVersion int
44 PropagationTimeout time.Duration
45 PollingInterval time.Duration
46 TTL int
47 HTTPClient *http.Client
48 }
49
50 // NewDefaultConfig returns a default configuration for the DNSProvider.
51 func NewDefaultConfig() *Config {
52 return &Config{
53 ServerName: env.GetOrDefaultString(EnvServerName, "localhost"),
54 APIVersion: env.GetOrDefaultInt(EnvAPIVersion, 0),
55 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
56 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),
57 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second),
58 HTTPClient: &http.Client{
59 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
60 },
61 }
62 }
63
64 // DNSProvider implements the challenge.Provider interface.
65 type DNSProvider struct {
66 config *Config
67 client *internal.Client
68 }
69
70 // NewDNSProvider returns a DNSProvider instance configured for pdns.
71 // Credentials must be passed in the environment variable:
72 // PDNS_API_URL and PDNS_API_KEY.
73 func NewDNSProvider() (*DNSProvider, error) {
74 values, err := env.Get(EnvAPIKey, EnvAPIURL)
75 if err != nil {
76 return nil, fmt.Errorf("pdns: %w", err)
77 }
78
79 hostURL, err := url.Parse(values[EnvAPIURL])
80 if err != nil {
81 return nil, fmt.Errorf("pdns: %w", err)
82 }
83
84 config := NewDefaultConfig()
85 config.Host = hostURL
86 config.APIKey = values[EnvAPIKey]
87
88 return NewDNSProviderConfig(config)
89 }
90
91 // NewDNSProviderConfig return a DNSProvider instance configured for pdns.
92 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
93 if config == nil {
94 return nil, errors.New("pdns: the configuration of the DNS provider is nil")
95 }
96
97 if config.APIKey == "" {
98 return nil, errors.New("pdns: API key missing")
99 }
100
101 if config.Host == nil || config.Host.Host == "" {
102 return nil, errors.New("pdns: API URL missing")
103 }
104
105 client := internal.NewClient(config.Host, config.ServerName, config.APIVersion, config.APIKey)
106
107 if config.HTTPClient != nil {
108 client.HTTPClient = config.HTTPClient
109 }
110
111 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
112
113 if config.APIVersion <= 0 {
114 err := client.SetAPIVersion(context.Background())
115 if err != nil {
116 log.Warnf("pdns: failed to get API version %v", err)
117 }
118 }
119
120 return &DNSProvider{config: config, client: client}, nil
121 }
122
123 // Timeout returns the timeout and interval to use when checking for DNS propagation.
124 // Adjusting here to cope with spikes in propagation times.
125 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
126 return d.config.PropagationTimeout, d.config.PollingInterval
127 }
128
129 // Present creates a TXT record to fulfill the dns-01 challenge.
130 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
131 ctx := context.Background()
132
133 info := dns01.GetChallengeInfo(domain, keyAuth)
134
135 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
136 if err != nil {
137 return fmt.Errorf("pdns: could not find zone for domain %q: %w", domain, err)
138 }
139
140 zone, err := d.client.GetHostedZone(ctx, authZone)
141 if err != nil {
142 return fmt.Errorf("pdns: get hosted zone for %s: %w", authZone, err)
143 }
144
145 name := info.EffectiveFQDN
146 if d.client.APIVersion() == 0 {
147 // pre-v1 API wants non-fqdn
148 name = dns01.UnFqdn(info.EffectiveFQDN)
149 }
150
151 // Look for existing records.
152 existingRRSet := findTxtRecord(zone, info.EffectiveFQDN)
153
154 var records []internal.Record
155 if existingRRSet != nil {
156 records = existingRRSet.Records
157 }
158
159 records = append(records, internal.Record{
160 Content: strconv.Quote(info.Value),
161 Disabled: false,
162
163 // pre-v1 API
164 Type: "TXT",
165 Name: name,
166 TTL: d.config.TTL,
167 })
168
169 rrSets := internal.RRSets{
170 RRSets: []internal.RRSet{{
171 Name: name,
172 ChangeType: "REPLACE",
173 Type: "TXT",
174 Kind: "Master",
175 TTL: d.config.TTL,
176 Records: records,
177 }},
178 }
179
180 err = d.client.UpdateRecords(ctx, zone, rrSets)
181 if err != nil {
182 return fmt.Errorf("pdns: update records: %w", err)
183 }
184
185 err = d.client.Notify(ctx, zone)
186 if err != nil {
187 return fmt.Errorf("pdns: notify: %w", err)
188 }
189
190 return nil
191 }
192
193 // CleanUp removes the TXT record matching the specified parameters.
194 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
195 ctx := context.Background()
196
197 info := dns01.GetChallengeInfo(domain, keyAuth)
198
199 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
200 if err != nil {
201 return fmt.Errorf("pdns: could not find zone for domain %q: %w", domain, err)
202 }
203
204 zone, err := d.client.GetHostedZone(ctx, authZone)
205 if err != nil {
206 return fmt.Errorf("pdns: get hosted zone for %s: %w", authZone, err)
207 }
208
209 // Look for existing records.
210 set := findTxtRecord(zone, info.EffectiveFQDN)
211 if set == nil {
212 return fmt.Errorf("pdns: no existing record found for %s", info.EffectiveFQDN)
213 }
214
215 var records []internal.Record
216
217 for _, r := range set.Records {
218 if r.Content != strconv.Quote(info.Value) {
219 records = append(records, r)
220 }
221 }
222
223 rrSet := internal.RRSet{
224 Name: set.Name,
225 Type: set.Type,
226 }
227
228 if len(records) > 0 {
229 rrSet.ChangeType = "REPLACE"
230 rrSet.TTL = d.config.TTL
231 rrSet.Records = records
232 } else {
233 rrSet.ChangeType = "DELETE"
234 }
235
236 err = d.client.UpdateRecords(ctx, zone, internal.RRSets{RRSets: []internal.RRSet{rrSet}})
237 if err != nil {
238 return fmt.Errorf("pdns: update records: %w", err)
239 }
240
241 err = d.client.Notify(ctx, zone)
242 if err != nil {
243 return fmt.Errorf("pdns: notify: %w", err)
244 }
245
246 return nil
247 }
248
249 func findTxtRecord(zone *internal.HostedZone, fqdn string) *internal.RRSet {
250 for _, set := range zone.RRSets {
251 if set.Type == "TXT" && (set.Name == dns01.UnFqdn(fqdn) || set.Name == fqdn) {
252 return &set
253 }
254 }
255
256 return nil
257 }
258