provider.go raw
1 // Package tecnocratica implements a DNS provider for solving the DNS-01 challenge using Tecnocrática.
2 package tecnocratica
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/tecnocratica/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 Token string
24
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 zoneIDs map[string]int
37 recordIDs map[string]int
38 recordIDsMu sync.Mutex
39 }
40
41 // NewDNSProviderConfig return a DNSProvider instance configured for Tecnocrática.
42 func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) {
43 if config == nil {
44 return nil, errors.New("the configuration of the DNS provider is nil")
45 }
46
47 if config.Token == "" {
48 return nil, errors.New("missing credentials")
49 }
50
51 client, err := internal.NewClient(config.Token)
52 if err != nil {
53 return nil, fmt.Errorf("create client: %w", err)
54 }
55
56 if config.HTTPClient != nil {
57 client.HTTPClient = config.HTTPClient
58 }
59
60 if baseURL != "" {
61 client.BaseURL, err = url.Parse(baseURL)
62 if err != nil {
63 return nil, err
64 }
65 }
66
67 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
68
69 return &DNSProvider{
70 config: config,
71 client: client,
72 zoneIDs: make(map[string]int),
73 recordIDs: make(map[string]int),
74 }, nil
75 }
76
77 // Timeout returns the timeout and interval to use when checking for DNS propagation.
78 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
79 return d.config.PropagationTimeout, d.config.PollingInterval
80 }
81
82 // Present creates a TXT record using the specified parameters.
83 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
84 ctx := context.Background()
85
86 info := dns01.GetChallengeInfo(domain, keyAuth)
87
88 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
89 if err != nil {
90 return fmt.Errorf("could not find zone for domain %q: %w", domain, err)
91 }
92
93 authZone = dns01.UnFqdn(authZone)
94
95 zone, err := d.findZone(ctx, authZone)
96 if err != nil {
97 return fmt.Errorf("%w", err)
98 }
99
100 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
101 if err != nil {
102 return fmt.Errorf("%w", err)
103 }
104
105 record := internal.Record{
106 Name: subDomain,
107 Type: "TXT",
108 Content: info.Value,
109 TTL: d.config.TTL,
110 }
111
112 newRecord, err := d.client.CreateRecord(ctx, zone.ID, record)
113 if err != nil {
114 return fmt.Errorf("create record: %w", err)
115 }
116
117 d.recordIDsMu.Lock()
118 d.zoneIDs[token] = zone.ID
119 d.recordIDs[token] = newRecord.ID
120 d.recordIDsMu.Unlock()
121
122 return nil
123 }
124
125 // CleanUp removes the TXT record matching the specified parameters.
126 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
127 info := dns01.GetChallengeInfo(domain, keyAuth)
128
129 d.recordIDsMu.Lock()
130 zoneID, zoneOK := d.zoneIDs[token]
131 recordID, recordOK := d.recordIDs[token]
132 d.recordIDsMu.Unlock()
133
134 if !zoneOK || !recordOK {
135 return fmt.Errorf("unknown record ID or zone ID for '%s' '%s'", info.EffectiveFQDN, token)
136 }
137
138 err := d.client.DeleteRecord(context.Background(), zoneID, recordID)
139 if err != nil {
140 return fmt.Errorf("delete record: fqdn=%s, zoneID=%d, recordID=%d: %w",
141 info.EffectiveFQDN, zoneID, recordID, err)
142 }
143
144 d.recordIDsMu.Lock()
145 delete(d.zoneIDs, token)
146 delete(d.recordIDs, token)
147 d.recordIDsMu.Unlock()
148
149 return nil
150 }
151
152 func (d *DNSProvider) findZone(ctx context.Context, zoneName string) (*internal.Zone, error) {
153 zones, err := d.client.GetZones(ctx)
154 if err != nil {
155 return nil, fmt.Errorf("get zones: %w", err)
156 }
157
158 for _, zone := range zones {
159 if zone.Name == zoneName || zone.HumanName == zoneName {
160 return &zone, nil
161 }
162 }
163
164 return nil, fmt.Errorf("zone not found: %s", zoneName)
165 }
166