manageengine.go raw
1 // Package manageengine implements a DNS provider for solving the DNS-01 challenge using ManageEngine CloudDNS.
2 package manageengine
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "slices"
9 "strings"
10 "time"
11
12 "github.com/go-acme/lego/v4/challenge/dns01"
13 "github.com/go-acme/lego/v4/platform/config/env"
14 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
15 "github.com/go-acme/lego/v4/providers/dns/manageengine/internal"
16 )
17
18 // Environment variables names.
19 const (
20 envNamespace = "MANAGEENGINE_"
21
22 EnvClientID = envNamespace + "CLIENT_ID"
23 EnvClientSecret = envNamespace + "CLIENT_SECRET"
24
25 EnvTTL = envNamespace + "TTL"
26 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
27 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
28 )
29
30 // Config is used to configure the creation of the DNSProvider.
31 type Config struct {
32 ClientID string
33 ClientSecret string
34
35 PropagationTimeout time.Duration
36 PollingInterval time.Duration
37 TTL int
38 }
39
40 // NewDefaultConfig returns a default configuration for the DNSProvider.
41 func NewDefaultConfig() *Config {
42 return &Config{
43 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
44 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
45 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
46 }
47 }
48
49 // DNSProvider implements the challenge.Provider interface.
50 type DNSProvider struct {
51 config *Config
52 client *internal.Client
53 }
54
55 // NewDNSProvider returns a DNSProvider instance configured for ManageEngine CloudDNS.
56 func NewDNSProvider() (*DNSProvider, error) {
57 values, err := env.Get(EnvClientID, EnvClientSecret)
58 if err != nil {
59 return nil, fmt.Errorf("manageengine: %w", err)
60 }
61
62 config := NewDefaultConfig()
63 config.ClientID = values[EnvClientID]
64 config.ClientSecret = values[EnvClientSecret]
65
66 return NewDNSProviderConfig(config)
67 }
68
69 // NewDNSProviderConfig return a DNSProvider instance configured for ManageEngine CloudDNS.
70 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
71 if config == nil {
72 return nil, errors.New("manageengine: the configuration of the DNS provider is nil")
73 }
74
75 if config.ClientID == "" || config.ClientSecret == "" {
76 return nil, errors.New("manageengine: credentials missing")
77 }
78
79 return &DNSProvider{
80 config: config,
81 client: internal.NewClient(
82 clientdebug.Wrap(
83 internal.CreateOAuthClient(context.Background(), config.ClientID, config.ClientSecret),
84 ),
85 ),
86 }, nil
87 }
88
89 // Present creates a TXT record using the specified parameters.
90 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
91 ctx := context.Background()
92
93 info := dns01.GetChallengeInfo(domain, keyAuth)
94
95 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
96 if err != nil {
97 return fmt.Errorf("manageengine: could not find zone for domain %q: %w", domain, err)
98 }
99
100 zoneID, err := d.findZoneID(ctx, authZone)
101 if err != nil {
102 return fmt.Errorf("manageengine: find zone ID: %w", err)
103 }
104
105 zoneRecord, err := d.findZoneRecord(ctx, zoneID, info.EffectiveFQDN)
106 if err != nil {
107 return fmt.Errorf("manageengine: find zone record: %w", err)
108 }
109
110 // Update the existing zone record.
111 if zoneRecord != nil {
112 for _, record := range zoneRecord.Records {
113 if slices.Contains(record.Values, info.Value) {
114 continue
115 }
116
117 zr := internal.ZoneRecord{
118 ZoneID: zoneID,
119 SpfTxtDomainID: zoneRecord.SpfTxtDomainID,
120 DomainName: info.EffectiveFQDN,
121 DomainTTL: d.config.TTL,
122 RecordType: "TXT",
123 Records: []internal.Record{{
124 Values: append(record.Values, info.Value),
125 DomainID: zoneRecord.SpfTxtDomainID,
126 }},
127 }
128
129 // Update the zone record.
130 err = d.client.UpdateZoneRecord(ctx, zr)
131 if err != nil {
132 return fmt.Errorf("manageengine: update zone record: %w", err)
133 }
134
135 return nil
136 }
137
138 return errors.New("manageengine: zone already contains the TXT record value")
139 }
140
141 // Create a new zone record.
142 record := internal.ZoneRecord{
143 ZoneID: zoneID,
144 DomainName: info.EffectiveFQDN,
145 DomainTTL: d.config.TTL,
146 RecordType: "TXT",
147 Records: []internal.Record{{
148 Values: []string{info.Value},
149 }},
150 }
151
152 err = d.client.CreateZoneRecord(ctx, zoneID, record)
153 if err != nil {
154 return fmt.Errorf("manageengine: create zone record: %w", err)
155 }
156
157 return nil
158 }
159
160 // CleanUp removes the TXT record matching the specified parameters.
161 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
162 ctx := context.Background()
163
164 info := dns01.GetChallengeInfo(domain, keyAuth)
165
166 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
167 if err != nil {
168 return fmt.Errorf("manageengine: could not find zone for domain %q: %w", domain, err)
169 }
170
171 zoneID, err := d.findZoneID(ctx, authZone)
172 if err != nil {
173 return fmt.Errorf("manageengine: find zone ID: %w", err)
174 }
175
176 zoneRecord, err := d.findZoneRecord(ctx, zoneID, info.EffectiveFQDN)
177 if err != nil {
178 return fmt.Errorf("manageengine: find zone record: %w", err)
179 }
180
181 for _, record := range zoneRecord.Records {
182 if !slices.Contains(record.Values, info.Value) {
183 continue
184 }
185
186 // Delete the zone record.
187 if len(record.Values) <= 1 {
188 err = d.client.DeleteZoneRecord(ctx, zoneID, zoneRecord.SpfTxtDomainID)
189 if err != nil {
190 return fmt.Errorf("manageengine: delete zone record: %w", err)
191 }
192
193 return nil
194 }
195
196 // Update the zone record.
197 var values []string
198
199 for _, value := range record.Values {
200 if value != info.Value {
201 values = append(values, value)
202 }
203 }
204
205 zr := internal.ZoneRecord{
206 ZoneID: zoneID,
207 SpfTxtDomainID: zoneRecord.SpfTxtDomainID,
208 DomainName: info.EffectiveFQDN,
209 DomainTTL: d.config.TTL,
210 RecordType: "TXT",
211 Records: []internal.Record{{
212 Values: values,
213 DomainID: zoneRecord.SpfTxtDomainID,
214 }},
215 }
216
217 err = d.client.UpdateZoneRecord(ctx, zr)
218 if err != nil {
219 return fmt.Errorf("manageengine: create zone record: %w", err)
220 }
221
222 return nil
223 }
224
225 return nil
226 }
227
228 // Timeout returns the timeout and interval to use when checking for DNS propagation.
229 // Adjusting here to cope with spikes in propagation times.
230 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
231 return d.config.PropagationTimeout, d.config.PollingInterval
232 }
233
234 func (d *DNSProvider) findZoneID(ctx context.Context, authZone string) (int, error) {
235 zones, err := d.client.GetAllZones(ctx)
236 if err != nil {
237 return 0, fmt.Errorf("get all zone groups: %w", err)
238 }
239
240 for _, zone := range zones {
241 if strings.EqualFold(zone.ZoneName, authZone) {
242 return zone.ZoneID, nil
243 }
244 }
245
246 return 0, fmt.Errorf("zone not found %s", authZone)
247 }
248
249 func (d *DNSProvider) findZoneRecord(ctx context.Context, zoneID int, fqdn string) (*internal.ZoneRecord, error) {
250 zoneRecords, err := d.client.GetAllZoneRecords(ctx, zoneID)
251 if err != nil {
252 return nil, fmt.Errorf("get all zone records: %w", err)
253 }
254
255 for _, zoneRecord := range zoneRecords {
256 if !strings.EqualFold(zoneRecord.DomainName, fqdn) {
257 continue
258 }
259
260 if strings.EqualFold(zoneRecord.RecordType, "TXT") {
261 return &zoneRecord, nil
262 }
263 }
264
265 return nil, nil
266 }
267