zoneee.go raw
1 // Package zoneee implements a DNS provider for solving the DNS-01 challenge through zone.ee.
2 package zoneee
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "net/url"
10 "time"
11
12 "github.com/go-acme/lego/v4/challenge"
13 "github.com/go-acme/lego/v4/challenge/dns01"
14 "github.com/go-acme/lego/v4/platform/config/env"
15 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
16 "github.com/go-acme/lego/v4/providers/dns/zoneee/internal"
17 )
18
19 // Environment variables names.
20 const (
21 envNamespace = "ZONEEE_"
22
23 EnvEndpoint = envNamespace + "ENDPOINT"
24 EnvAPIUser = envNamespace + "API_USER"
25 EnvAPIKey = envNamespace + "API_KEY"
26
27 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
28 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
29 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
30 )
31
32 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
33
34 // Config is used to configure the creation of the DNSProvider.
35 type Config struct {
36 Endpoint *url.URL
37 Username string
38 APIKey string
39 PropagationTimeout time.Duration
40 PollingInterval time.Duration
41 HTTPClient *http.Client
42 }
43
44 // NewDefaultConfig returns a default configuration for the DNSProvider.
45 func NewDefaultConfig() *Config {
46 endpoint, _ := url.Parse(internal.DefaultEndpoint)
47
48 return &Config{
49 Endpoint: endpoint,
50 // zone.ee can take up to 5min to propagate according to the support
51 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
52 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),
53 HTTPClient: &http.Client{
54 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
55 },
56 }
57 }
58
59 // DNSProvider implements the challenge.Provider interface.
60 type DNSProvider struct {
61 config *Config
62 client *internal.Client
63 }
64
65 // NewDNSProvider returns a DNSProvider instance.
66 func NewDNSProvider() (*DNSProvider, error) {
67 values, err := env.Get(EnvAPIUser, EnvAPIKey)
68 if err != nil {
69 return nil, fmt.Errorf("zoneee: %w", err)
70 }
71
72 rawEndpoint := env.GetOrDefaultString(EnvEndpoint, internal.DefaultEndpoint)
73
74 endpoint, err := url.Parse(rawEndpoint)
75 if err != nil {
76 return nil, fmt.Errorf("zoneee: %w", err)
77 }
78
79 config := NewDefaultConfig()
80 config.Username = values[EnvAPIUser]
81 config.APIKey = values[EnvAPIKey]
82 config.Endpoint = endpoint
83
84 return NewDNSProviderConfig(config)
85 }
86
87 // NewDNSProviderConfig return a DNSProvider instance configured for Zone.ee.
88 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
89 if config == nil {
90 return nil, errors.New("zoneee: the configuration of the DNS provider is nil")
91 }
92
93 if config.Username == "" {
94 return nil, errors.New("zoneee: credentials missing: username")
95 }
96
97 if config.APIKey == "" {
98 return nil, errors.New("zoneee: credentials missing: API key")
99 }
100
101 if config.Endpoint == nil {
102 return nil, errors.New("zoneee: the endpoint is missing")
103 }
104
105 client := internal.NewClient(config.Username, 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.Endpoint != nil {
114 client.BaseURL = config.Endpoint
115 }
116
117 return &DNSProvider{config: config, client: client}, nil
118 }
119
120 // Timeout returns the timeout and interval to use when checking for DNS propagation.
121 // Adjusting here to cope with spikes in propagation times.
122 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
123 return d.config.PropagationTimeout, d.config.PollingInterval
124 }
125
126 // Present creates a TXT record to fulfill the dns-01 challenge.
127 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
128 info := dns01.GetChallengeInfo(domain, keyAuth)
129
130 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
131 if err != nil {
132 return fmt.Errorf("zoneee: could not find zone for domain %q: %w", domain, err)
133 }
134
135 authZone = dns01.UnFqdn(authZone)
136
137 record := internal.TXTRecord{
138 Name: dns01.UnFqdn(info.EffectiveFQDN),
139 Destination: info.Value,
140 }
141
142 _, err = d.client.AddTxtRecord(context.Background(), authZone, record)
143 if err != nil {
144 return fmt.Errorf("zoneee: %w", err)
145 }
146
147 return nil
148 }
149
150 // CleanUp removes the TXT record previously created.
151 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
152 info := dns01.GetChallengeInfo(domain, keyAuth)
153
154 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
155 if err != nil {
156 return fmt.Errorf("zoneee: could not find zone for domain %q: %w", domain, err)
157 }
158
159 authZone = dns01.UnFqdn(authZone)
160
161 ctx := context.Background()
162
163 records, err := d.client.GetTxtRecords(ctx, authZone)
164 if err != nil {
165 return fmt.Errorf("zoneee: %w", err)
166 }
167
168 var id string
169
170 for _, record := range records {
171 if record.Destination == info.Value {
172 id = record.ID
173 }
174 }
175
176 if id == "" {
177 return fmt.Errorf("zoneee: txt record does not exist for %s", info.Value)
178 }
179
180 if err = d.client.RemoveTxtRecord(ctx, authZone, id); err != nil {
181 return fmt.Errorf("zoneee: %w", err)
182 }
183
184 return nil
185 }
186