jdcloud.go raw
1 // Package jdcloud implements a DNS provider for solving the DNS-01 challenge using JD Cloud.
2 package jdcloud
3
4 import (
5 "errors"
6 "fmt"
7 "strconv"
8 "sync"
9 "time"
10
11 "github.com/go-acme/jdcloud-sdk-go/core"
12 "github.com/go-acme/jdcloud-sdk-go/services/domainservice/apis"
13 jdcclient "github.com/go-acme/jdcloud-sdk-go/services/domainservice/client"
14 domainservice "github.com/go-acme/jdcloud-sdk-go/services/domainservice/models"
15 "github.com/go-acme/lego/v4/challenge/dns01"
16 "github.com/go-acme/lego/v4/platform/config/env"
17 )
18
19 // Environment variables names.
20 const (
21 envNamespace = "JDCLOUD_"
22
23 EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID"
24 EnvAccessKeySecret = envNamespace + "ACCESS_KEY_SECRET"
25 EnvRegionID = envNamespace + "REGION_ID"
26
27 EnvTTL = envNamespace + "TTL"
28 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
29 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
30 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
31 )
32
33 // Config is used to configure the creation of the DNSProvider.
34 type Config struct {
35 AccessKeyID string
36 AccessKeySecret string
37 RegionID string
38
39 PropagationTimeout time.Duration
40 PollingInterval time.Duration
41 TTL int
42 HTTPTimeout time.Duration
43 }
44
45 // NewDefaultConfig returns a default configuration for the DNSProvider.
46 func NewDefaultConfig() *Config {
47 return &Config{
48 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
49 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
50 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
51 HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
52 }
53 }
54
55 // DNSProvider implements the challenge.Provider interface.
56 type DNSProvider struct {
57 config *Config
58 client *jdcclient.DomainserviceClient
59
60 recordIDs map[string]int
61 domainIDs map[string]int
62 recordIDsMu sync.Mutex
63 }
64
65 // NewDNSProvider returns a DNSProvider instance configured for JD Cloud.
66 func NewDNSProvider() (*DNSProvider, error) {
67 values, err := env.Get(EnvAccessKeyID, EnvAccessKeySecret)
68 if err != nil {
69 return nil, fmt.Errorf("jdcloud: %w", err)
70 }
71
72 config := NewDefaultConfig()
73 config.AccessKeyID = values[EnvAccessKeyID]
74 config.AccessKeySecret = values[EnvAccessKeySecret]
75
76 // https://docs.jdcloud.com/en/common-declaration/api/introduction#Region%20Code
77 config.RegionID = env.GetOrDefaultString(EnvRegionID, "cn-north-1")
78
79 return NewDNSProviderConfig(config)
80 }
81
82 // NewDNSProviderConfig return a DNSProvider instance configured for JD Cloud.
83 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
84 if config == nil {
85 return nil, errors.New("jdcloud: the configuration of the DNS provider is nil")
86 }
87
88 if config.AccessKeyID == "" || config.AccessKeySecret == "" {
89 return nil, errors.New("jdcloud: missing credentials")
90 }
91
92 cred := core.NewCredentials(config.AccessKeyID, config.AccessKeySecret)
93
94 client := jdcclient.NewDomainserviceClient(cred)
95 client.DisableLogger()
96 client.Config.SetTimeout(config.HTTPTimeout)
97
98 return &DNSProvider{
99 config: config,
100 client: client,
101 recordIDs: make(map[string]int),
102 domainIDs: make(map[string]int),
103 }, nil
104 }
105
106 // Present creates a TXT record using the specified parameters.
107 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
108 info := dns01.GetChallengeInfo(domain, keyAuth)
109
110 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
111 if err != nil {
112 return fmt.Errorf("jdcloud: could not find zone for domain %q: %w", domain, err)
113 }
114
115 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
116 if err != nil {
117 return fmt.Errorf("jdcloud: %w", err)
118 }
119
120 zone, err := d.findZone(dns01.UnFqdn(authZone))
121 if err != nil {
122 return fmt.Errorf("jdcloud: %w", err)
123 }
124
125 // https://docs.jdcloud.com/cn/jd-cloud-dns/api/createresourcerecord
126 crrr := apis.NewCreateResourceRecordRequestWithAllParams(
127 d.config.RegionID,
128 strconv.Itoa(zone.Id),
129 &domainservice.AddRR{
130 HostRecord: subDomain,
131 HostValue: info.Value,
132 Ttl: d.config.TTL,
133 Type: "TXT",
134 ViewValue: -1,
135 },
136 )
137
138 record, err := jdcclient.CreateResourceRecord(d.client, crrr)
139 if err != nil {
140 return fmt.Errorf("jdcloud: create resource record: %w", err)
141 }
142
143 d.recordIDsMu.Lock()
144 d.domainIDs[token] = zone.Id
145 d.recordIDs[token] = record.Result.DataList.Id
146 d.recordIDsMu.Unlock()
147
148 return nil
149 }
150
151 // CleanUp removes the TXT record matching the specified parameters.
152 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
153 info := dns01.GetChallengeInfo(domain, keyAuth)
154
155 d.recordIDsMu.Lock()
156 recordID, recordOK := d.recordIDs[token]
157 domainID, domainOK := d.domainIDs[token]
158 d.recordIDsMu.Unlock()
159
160 if !recordOK {
161 return fmt.Errorf("jdcloud: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token)
162 }
163
164 if !domainOK {
165 return fmt.Errorf("jdcloud: unknown domain ID for '%s' '%s'", info.EffectiveFQDN, token)
166 }
167
168 // https://docs.jdcloud.com/cn/jd-cloud-dns/api/deleteresourcerecord
169 drrr := apis.NewDeleteResourceRecordRequestWithAllParams(
170 d.config.RegionID,
171 strconv.Itoa(domainID),
172 strconv.Itoa(recordID),
173 )
174
175 _, err := jdcclient.DeleteResourceRecord(d.client, drrr)
176 if err != nil {
177 return fmt.Errorf("jdcloud: delete resource record: %w", err)
178 }
179
180 return nil
181 }
182
183 // Timeout returns the timeout and interval to use when checking for DNS propagation.
184 // Adjusting here to cope with spikes in propagation times.
185 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
186 return d.config.PropagationTimeout, d.config.PollingInterval
187 }
188
189 func (d *DNSProvider) findZone(zone string) (*domainservice.DomainInfo, error) {
190 // https://docs.jdcloud.com/cn/jd-cloud-dns/api/describedomains
191 ddr := apis.NewDescribeDomainsRequestWithoutParam()
192 ddr.SetRegionId(d.config.RegionID)
193 ddr.SetPageNumber(1)
194 ddr.SetPageSize(10)
195 ddr.SetDomainName(zone)
196
197 for {
198 response, err := jdcclient.DescribeDomains(d.client, ddr)
199 if err != nil {
200 return nil, fmt.Errorf("describe domains: %w", err)
201 }
202
203 for _, d := range response.Result.DataList {
204 if d.DomainName == zone {
205 return &d, nil
206 }
207 }
208
209 if len(response.Result.DataList) < ddr.PageSize || response.Result.TotalPage <= ddr.PageNumber {
210 break
211 }
212
213 ddr.SetPageNumber(ddr.PageNumber + 1)
214 }
215
216 return nil, errors.New("zone not found")
217 }
218