provider.go raw
1 // Package hostingde implements a DNS provider for solving the DNS-01 challenge using hosting.de.
2 package hostingde
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "net/url"
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/providers/dns/internal/clientdebug"
16 "github.com/go-acme/lego/v4/providers/dns/internal/hostingde/internal"
17 )
18
19 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
20
21 // Config is used to configure the creation of the DNSProvider.
22 type Config struct {
23 APIKey string
24 ZoneName string
25 PropagationTimeout time.Duration
26 PollingInterval time.Duration
27 TTL int
28 HTTPClient *http.Client
29 }
30
31 // DNSProvider implements the challenge.Provider interface.
32 type DNSProvider struct {
33 config *Config
34 client *internal.Client
35
36 recordIDs map[string]string
37 recordIDsMu sync.Mutex
38 }
39
40 // NewDNSProviderConfig return a DNSProvider instance configured for hosting.de.
41 func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) {
42 if config == nil {
43 return nil, errors.New("the configuration of the DNS provider is nil")
44 }
45
46 if config.APIKey == "" {
47 return nil, errors.New("API key missing")
48 }
49
50 client := internal.NewClient(config.APIKey)
51
52 if baseURL != "" {
53 client.BaseURL, _ = url.Parse(baseURL)
54 }
55
56 if config.HTTPClient != nil {
57 client.HTTPClient = config.HTTPClient
58 }
59
60 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
61
62 return &DNSProvider{
63 config: config,
64 client: client,
65 recordIDs: make(map[string]string),
66 }, nil
67 }
68
69 // Timeout returns the timeout and interval to use when checking for DNS propagation.
70 // Adjusting here to cope with spikes in propagation times.
71 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
72 return d.config.PropagationTimeout, d.config.PollingInterval
73 }
74
75 // Present creates a TXT record to fulfill the dns-01 challenge.
76 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
77 info := dns01.GetChallengeInfo(domain, keyAuth)
78
79 zoneName, err := d.getZoneName(info.EffectiveFQDN)
80 if err != nil {
81 return fmt.Errorf("could not find zone for domain %q: %w", domain, err)
82 }
83
84 ctx := context.Background()
85
86 // get the ZoneConfig for that domain
87 zonesFind := internal.ZoneConfigsFindRequest{
88 Filter: internal.Filter{Field: "zoneName", Value: zoneName},
89 Limit: 1,
90 Page: 1,
91 }
92
93 zoneConfig, err := d.client.GetZone(ctx, zonesFind)
94 if err != nil {
95 return err
96 }
97
98 zoneConfig.Name = zoneName
99
100 rec := []internal.DNSRecord{{
101 Type: "TXT",
102 Name: dns01.UnFqdn(info.EffectiveFQDN),
103 Content: info.Value,
104 TTL: d.config.TTL,
105 }}
106
107 req := internal.ZoneUpdateRequest{
108 ZoneConfig: *zoneConfig,
109 RecordsToAdd: rec,
110 }
111
112 response, err := d.client.UpdateZone(ctx, req)
113 if err != nil {
114 return err
115 }
116
117 for _, record := range response.Records {
118 if record.Name == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == fmt.Sprintf(`%q`, info.Value) {
119 d.recordIDsMu.Lock()
120 d.recordIDs[info.EffectiveFQDN] = record.ID
121 d.recordIDsMu.Unlock()
122 }
123 }
124
125 if d.recordIDs[info.EffectiveFQDN] == "" {
126 return fmt.Errorf("error getting ID of just created record, for domain %s", domain)
127 }
128
129 return nil
130 }
131
132 // CleanUp removes the TXT record matching the specified parameters.
133 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
134 info := dns01.GetChallengeInfo(domain, keyAuth)
135
136 zoneName, err := d.getZoneName(info.EffectiveFQDN)
137 if err != nil {
138 return fmt.Errorf("could not find zone for domain %q: %w", domain, err)
139 }
140
141 ctx := context.Background()
142
143 // get the ZoneConfig for that domain
144 zonesFind := internal.ZoneConfigsFindRequest{
145 Filter: internal.Filter{Field: "zoneName", Value: zoneName},
146 Limit: 1,
147 Page: 1,
148 }
149
150 zoneConfig, err := d.client.GetZone(ctx, zonesFind)
151 if err != nil {
152 return err
153 }
154
155 zoneConfig.Name = zoneName
156
157 rec := []internal.DNSRecord{{
158 Type: "TXT",
159 Name: dns01.UnFqdn(info.EffectiveFQDN),
160 Content: `"` + info.Value + `"`,
161 }}
162
163 req := internal.ZoneUpdateRequest{
164 ZoneConfig: *zoneConfig,
165 RecordsToDelete: rec,
166 }
167
168 _, err = d.client.UpdateZone(ctx, req)
169 if err != nil {
170 return err
171 }
172
173 // Delete record ID from map
174 d.recordIDsMu.Lock()
175 delete(d.recordIDs, info.EffectiveFQDN)
176 d.recordIDsMu.Unlock()
177
178 return nil
179 }
180
181 func (d *DNSProvider) getZoneName(fqdn string) (string, error) {
182 if d.config.ZoneName != "" {
183 return d.config.ZoneName, nil
184 }
185
186 zoneName, err := dns01.FindZoneByFqdn(fqdn)
187 if err != nil {
188 return "", fmt.Errorf("could not find zone for %s: %w", fqdn, err)
189 }
190
191 if zoneName == "" {
192 return "", errors.New("empty zone name")
193 }
194
195 return dns01.UnFqdn(zoneName), nil
196 }
197