provider_dmapi.go raw
1 package joker
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "time"
8
9 "github.com/go-acme/lego/v4/challenge"
10 "github.com/go-acme/lego/v4/challenge/dns01"
11 "github.com/go-acme/lego/v4/log"
12 "github.com/go-acme/lego/v4/platform/config/env"
13 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
14 "github.com/go-acme/lego/v4/providers/dns/joker/internal/dmapi"
15 )
16
17 var _ challenge.ProviderTimeout = (*dmapiProvider)(nil)
18
19 // dmapiProvider implements the challenge.Provider interface.
20 type dmapiProvider struct {
21 config *Config
22 client *dmapi.Client
23 }
24
25 // newDmapiProvider returns a DNSProvider instance configured for Joker.
26 // Credentials must be passed in the environment variable: JOKER_USERNAME, JOKER_PASSWORD or JOKER_API_KEY.
27 func newDmapiProvider() (*dmapiProvider, error) {
28 values, err := env.Get(EnvAPIKey)
29 if err != nil {
30 var errU error
31
32 values, errU = env.Get(EnvUsername, EnvPassword)
33 if errU != nil {
34 //nolint:errorlint // false-positive
35 return nil, fmt.Errorf("joker: %v or %v", errU, err)
36 }
37 }
38
39 config := NewDefaultConfig()
40 config.APIKey = values[EnvAPIKey]
41 config.Username = values[EnvUsername]
42 config.Password = values[EnvPassword]
43
44 return newDmapiProviderConfig(config)
45 }
46
47 // newDmapiProviderConfig return a DNSProvider instance configured for Joker.
48 func newDmapiProviderConfig(config *Config) (*dmapiProvider, error) {
49 if config == nil {
50 return nil, errors.New("joker: the configuration of the DNS provider is nil")
51 }
52
53 if config.APIKey == "" {
54 if config.Username == "" || config.Password == "" {
55 return nil, errors.New("joker: credentials missing")
56 }
57 }
58
59 client := dmapi.NewClient(dmapi.AuthInfo{
60 APIKey: config.APIKey,
61 Username: config.Username,
62 Password: config.Password,
63 })
64
65 client.Debug = config.Debug
66
67 if config.HTTPClient != nil {
68 client.HTTPClient = config.HTTPClient
69 }
70
71 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
72
73 return &dmapiProvider{config: config, client: client}, nil
74 }
75
76 // Timeout returns the timeout and interval to use when checking for DNS propagation.
77 // Adjusting here to cope with spikes in propagation times.
78 func (d *dmapiProvider) 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 *dmapiProvider) Present(domain, token, keyAuth string) error {
84 info := dns01.GetChallengeInfo(domain, keyAuth)
85
86 zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
87 if err != nil {
88 return fmt.Errorf("joker: could not find zone for domain %q: %w", domain, err)
89 }
90
91 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)
92 if err != nil {
93 return fmt.Errorf("joker: %w", err)
94 }
95
96 if d.config.Debug {
97 log.Infof("[%s] joker: adding TXT record %q to zone %q with value %q", domain, subDomain, zone, info.Value)
98 }
99
100 ctx, err := d.client.CreateAuthenticatedContext(context.Background())
101 if err != nil {
102 return err
103 }
104
105 response, err := d.client.GetZone(ctx, zone)
106 if err != nil || response.StatusCode != 0 {
107 return formatResponseError(response, err)
108 }
109
110 dnsZone := dmapi.AddTxtEntryToZone(response.Body, subDomain, info.Value, d.config.TTL)
111
112 response, err = d.client.PutZone(ctx, zone, dnsZone)
113 if err != nil || response.StatusCode != 0 {
114 return formatResponseError(response, err)
115 }
116
117 return nil
118 }
119
120 // CleanUp removes the TXT record matching the specified parameters.
121 func (d *dmapiProvider) CleanUp(domain, token, keyAuth string) error {
122 info := dns01.GetChallengeInfo(domain, keyAuth)
123
124 zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
125 if err != nil {
126 return fmt.Errorf("joker: could not find zone for domain %q: %w", domain, err)
127 }
128
129 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)
130 if err != nil {
131 return fmt.Errorf("joker: %w", err)
132 }
133
134 if d.config.Debug {
135 log.Infof("[%s] joker: removing entry %q from zone %q", domain, subDomain, zone)
136 }
137
138 ctx, err := d.client.CreateAuthenticatedContext(context.Background())
139 if err != nil {
140 return err
141 }
142
143 defer func() {
144 // Try to log out in case of errors
145 _, _ = d.client.Logout(ctx)
146 }()
147
148 response, err := d.client.GetZone(ctx, zone)
149 if err != nil || response.StatusCode != 0 {
150 return formatResponseError(response, err)
151 }
152
153 dnsZone, modified := dmapi.RemoveTxtEntryFromZone(response.Body, subDomain)
154 if modified {
155 response, err = d.client.PutZone(ctx, zone, dnsZone)
156 if err != nil || response.StatusCode != 0 {
157 return formatResponseError(response, err)
158 }
159 }
160
161 response, err = d.client.Logout(ctx)
162 if err != nil {
163 return formatResponseError(response, err)
164 }
165
166 return nil
167 }
168
169 // formatResponseError formats error with optional details from DMAPI response.
170 func formatResponseError(response *dmapi.Response, err error) error {
171 if response != nil {
172 return fmt.Errorf("joker: DMAPI error: %w Response: %v", err, response.Headers)
173 }
174
175 return fmt.Errorf("joker: DMAPI error: %w", err)
176 }
177