provider.go raw
1 // Package active24 implements a DNS provider for solving the DNS-01 challenge using Active24.
2 package active24
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "strconv"
10 "time"
11
12 "github.com/go-acme/lego/v4/challenge/dns01"
13 "github.com/go-acme/lego/v4/providers/dns/internal/active24/internal"
14 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
15 )
16
17 // Config is used to configure the creation of the DNSProvider.
18 type Config struct {
19 APIKey string
20 Secret string
21
22 PropagationTimeout time.Duration
23 PollingInterval time.Duration
24 TTL int
25 HTTPClient *http.Client
26 }
27
28 // DNSProvider implements the challenge.Provider interface.
29 type DNSProvider struct {
30 config *Config
31 client *internal.Client
32 }
33
34 // NewDNSProviderConfig return a DNSProvider instance configured for Active24.
35 func NewDNSProviderConfig(config *Config, baseAPIDomain string) (*DNSProvider, error) {
36 if config == nil {
37 return nil, errors.New("the configuration of the DNS provider is nil")
38 }
39
40 client, err := internal.NewClient(baseAPIDomain, config.APIKey, config.Secret)
41 if err != nil {
42 return nil, err
43 }
44
45 if config.HTTPClient != nil {
46 client.HTTPClient = config.HTTPClient
47 }
48
49 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
50
51 return &DNSProvider{
52 config: config,
53 client: client,
54 }, nil
55 }
56
57 // Present creates a TXT record using the specified parameters.
58 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
59 ctx := context.Background()
60
61 info := dns01.GetChallengeInfo(domain, keyAuth)
62
63 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
64 if err != nil {
65 return fmt.Errorf("could not find zone for domain %q: %w", domain, err)
66 }
67
68 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
69 if err != nil {
70 return err
71 }
72
73 serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone))
74 if err != nil {
75 return fmt.Errorf("find service ID: %w", err)
76 }
77
78 record := internal.Record{
79 Type: "TXT",
80 Name: subDomain,
81 Content: info.Value,
82 TTL: d.config.TTL,
83 }
84
85 err = d.client.CreateRecord(ctx, strconv.Itoa(serviceID), record)
86 if err != nil {
87 return fmt.Errorf("create record: %w", err)
88 }
89
90 return nil
91 }
92
93 // CleanUp removes the TXT record matching the specified parameters.
94 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
95 ctx := context.Background()
96
97 info := dns01.GetChallengeInfo(domain, keyAuth)
98
99 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
100 if err != nil {
101 return fmt.Errorf("could not find zone for domain %q: %w", domain, err)
102 }
103
104 serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone))
105 if err != nil {
106 return fmt.Errorf("find service ID: %w", err)
107 }
108
109 recordID, err := d.findRecordID(ctx, strconv.Itoa(serviceID), info)
110 if err != nil {
111 return fmt.Errorf("find record ID: %w", err)
112 }
113
114 err = d.client.DeleteRecord(ctx, strconv.Itoa(serviceID), strconv.Itoa(recordID))
115 if err != nil {
116 return fmt.Errorf("delete record %w", err)
117 }
118
119 return nil
120 }
121
122 // Timeout returns the timeout and interval to use when checking for DNS propagation.
123 // Adjusting here to cope with spikes in propagation times.
124 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
125 return d.config.PropagationTimeout, d.config.PollingInterval
126 }
127
128 func (d *DNSProvider) findServiceID(ctx context.Context, domain string) (int, error) {
129 services, err := d.client.GetServices(ctx)
130 if err != nil {
131 return 0, fmt.Errorf("get services: %w", err)
132 }
133
134 for _, service := range services {
135 if service.ServiceName != "domain" {
136 continue
137 }
138
139 if service.Name != domain {
140 continue
141 }
142
143 return service.ID, nil
144 }
145
146 return 0, fmt.Errorf("service not found for domain: %s", domain)
147 }
148
149 func (d *DNSProvider) findRecordID(ctx context.Context, serviceID string, info dns01.ChallengeInfo) (int, error) {
150 // NOTE(ldez): Despite the API documentation, the filter doesn't seem to work.
151 filter := internal.RecordFilter{
152 Name: dns01.UnFqdn(info.EffectiveFQDN),
153 Type: []string{"TXT"},
154 Content: info.Value,
155 }
156
157 records, err := d.client.GetRecords(ctx, serviceID, filter)
158 if err != nil {
159 return 0, fmt.Errorf("get records: %w", err)
160 }
161
162 for _, record := range records {
163 if record.Type != "TXT" {
164 continue
165 }
166
167 if record.Name != dns01.UnFqdn(info.EffectiveFQDN) {
168 continue
169 }
170
171 if record.Content != info.Value {
172 continue
173 }
174
175 return record.ID, nil
176 }
177
178 return 0, errors.New("no record found")
179 }
180