provider.go raw
1 package ionos
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8 "net/url"
9 "strconv"
10 "strings"
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 ionos "github.com/go-acme/lego/v4/providers/dns/internal/ionos/internal"
17 )
18
19 const MinTTL = 300
20
21 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
22
23 // Config is used to configure the creation of the DNSProvider.
24 type Config struct {
25 APIKey string
26 PropagationTimeout time.Duration
27 PollingInterval time.Duration
28 TTL int
29 HTTPClient *http.Client
30 }
31
32 // DNSProvider implements the challenge.Provider interface.
33 type DNSProvider struct {
34 config *Config
35 client *ionos.Client
36 }
37
38 // NewDNSProviderConfig return a DNSProvider instance configured for Ionos.
39 func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) {
40 if config == nil {
41 return nil, errors.New("the configuration of the DNS provider is nil")
42 }
43
44 if config.APIKey == "" {
45 return nil, errors.New("credentials missing")
46 }
47
48 if config.TTL < MinTTL {
49 return nil, fmt.Errorf("invalid TTL, TTL (%d) must be greater than %d", config.TTL, MinTTL)
50 }
51
52 client, err := ionos.NewClient(config.APIKey)
53 if err != nil {
54 return nil, err
55 }
56
57 if baseURL != "" {
58 client.BaseURL, _ = url.Parse(baseURL)
59 }
60
61 if config.HTTPClient != nil {
62 client.HTTPClient = config.HTTPClient
63 }
64
65 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
66
67 return &DNSProvider{config: config, client: client}, nil
68 }
69
70 // Timeout returns the timeout and interval to use when checking for DNS propagation.
71 // Adjusting here to cope with spikes in propagation times.
72 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
73 return d.config.PropagationTimeout, d.config.PollingInterval
74 }
75
76 // Present creates a TXT record using the specified parameters.
77 func (d *DNSProvider) Present(domain, _, keyAuth string) error {
78 info := dns01.GetChallengeInfo(domain, keyAuth)
79
80 ctx := context.Background()
81
82 zones, err := d.client.ListZones(ctx)
83 if err != nil {
84 return fmt.Errorf("failed to get zones: %w", err)
85 }
86
87 name := dns01.UnFqdn(info.EffectiveFQDN)
88
89 zone := findZone(zones, name)
90 if zone == nil {
91 return errors.New("no matching zone found for domain")
92 }
93
94 filter := &ionos.RecordsFilter{
95 Suffix: name,
96 RecordType: "TXT",
97 }
98
99 records, err := d.client.GetRecords(ctx, zone.ID, filter)
100 if err != nil {
101 return fmt.Errorf("failed to get records (zone=%s): %w", zone.ID, err)
102 }
103
104 records = append(records, ionos.Record{
105 Name: name,
106 Content: info.Value,
107 TTL: d.config.TTL,
108 Type: "TXT",
109 })
110
111 err = d.client.ReplaceRecords(ctx, zone.ID, records)
112 if err != nil {
113 return fmt.Errorf("failed to create/update records (zone=%s): %w", zone.ID, err)
114 }
115
116 return nil
117 }
118
119 // CleanUp removes the TXT record matching the specified parameters.
120 func (d *DNSProvider) CleanUp(domain, _, 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("failed to get zones: %w", err)
128 }
129
130 name := dns01.UnFqdn(info.EffectiveFQDN)
131
132 zone := findZone(zones, name)
133 if zone == nil {
134 return errors.New("no matching zone found for domain")
135 }
136
137 filter := &ionos.RecordsFilter{
138 Suffix: name,
139 RecordType: "TXT",
140 }
141
142 records, err := d.client.GetRecords(ctx, zone.ID, filter)
143 if err != nil {
144 return fmt.Errorf("failed to get records (zone=%s): %w", zone.ID, err)
145 }
146
147 for _, record := range records {
148 if record.Name == name && record.Content == strconv.Quote(info.Value) {
149 err = d.client.RemoveRecord(ctx, zone.ID, record.ID)
150 if err != nil {
151 return fmt.Errorf("failed to remove record (zone=%s, record=%s): %w", zone.ID, record.ID, err)
152 }
153
154 return nil
155 }
156 }
157
158 return fmt.Errorf("failed to remove record, record not found (zone=%s, domain=%s, fqdn=%s, value=%s)", zone.ID, domain, info.EffectiveFQDN, info.Value)
159 }
160
161 func findZone(zones []ionos.Zone, domain string) *ionos.Zone {
162 var result *ionos.Zone
163
164 for _, zone := range zones {
165 if zone.Name != "" && strings.HasSuffix(domain, zone.Name) {
166 if result == nil || len(zone.Name) > len(result.Name) {
167 result = &zone
168 }
169 }
170 }
171
172 return result
173 }
174