dnspod.go raw
1 // Package dnspod implements a DNS provider for solving the DNS-01 challenge using dnspod DNS.
2 package dnspod
3
4 import (
5 "errors"
6 "fmt"
7 "net/http"
8 "strconv"
9 "time"
10
11 "github.com/go-acme/lego/v4/challenge"
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/nrdcg/dnspod-go"
16 )
17
18 // Environment variables names.
19 const (
20 envNamespace = "DNSPOD_"
21
22 EnvAPIKey = envNamespace + "API_KEY"
23
24 EnvTTL = envNamespace + "TTL"
25 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
26 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
27 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
28 )
29
30 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
31
32 // Config is used to configure the creation of the DNSProvider.
33 type Config struct {
34 LoginToken string
35 TTL int
36 PropagationTimeout time.Duration
37 PollingInterval time.Duration
38 HTTPClient *http.Client
39 }
40
41 // NewDefaultConfig returns a default configuration for the DNSProvider.
42 func NewDefaultConfig() *Config {
43 return &Config{
44 TTL: env.GetOrDefaultInt(EnvTTL, 600),
45 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
46 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
47 HTTPClient: &http.Client{
48 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
49 },
50 }
51 }
52
53 // DNSProvider implements the challenge.Provider interface.
54 type DNSProvider struct {
55 config *Config
56 client *dnspod.Client
57 }
58
59 // NewDNSProvider returns a DNSProvider instance configured for dnspod.
60 // Credentials must be passed in the environment variables: DNSPOD_API_KEY.
61 func NewDNSProvider() (*DNSProvider, error) {
62 values, err := env.Get(EnvAPIKey)
63 if err != nil {
64 return nil, fmt.Errorf("dnspod: %w", err)
65 }
66
67 config := NewDefaultConfig()
68 config.LoginToken = values[EnvAPIKey]
69
70 return NewDNSProviderConfig(config)
71 }
72
73 // NewDNSProviderConfig return a DNSProvider instance configured for dnspod.
74 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
75 if config == nil {
76 return nil, errors.New("dnspod: the configuration of the DNS provider is nil")
77 }
78
79 if config.LoginToken == "" {
80 return nil, errors.New("dnspod: credentials missing")
81 }
82
83 params := dnspod.CommonParams{LoginToken: config.LoginToken, Format: "json"}
84
85 client := dnspod.NewClient(params)
86
87 if config.HTTPClient != nil {
88 client.HTTPClient = config.HTTPClient
89 }
90
91 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
92
93 return &DNSProvider{client: client, config: config}, nil
94 }
95
96 // Present creates a TXT record to fulfill the dns-01 challenge.
97 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
98 info := dns01.GetChallengeInfo(domain, keyAuth)
99
100 zoneID, zoneName, err := d.getHostedZone(info.EffectiveFQDN)
101 if err != nil {
102 return err
103 }
104
105 recordAttributes, err := d.newTxtRecord(zoneName, info.EffectiveFQDN, info.Value, d.config.TTL)
106 if err != nil {
107 return err
108 }
109
110 _, _, err = d.client.Records.Create(zoneID, *recordAttributes)
111 if err != nil {
112 return fmt.Errorf("API call failed: %w", err)
113 }
114
115 return nil
116 }
117
118 // CleanUp removes the TXT record matching the specified parameters.
119 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
120 info := dns01.GetChallengeInfo(domain, keyAuth)
121
122 zoneID, zoneName, err := d.getHostedZone(info.EffectiveFQDN)
123 if err != nil {
124 return err
125 }
126
127 records, err := d.findTxtRecords(info.EffectiveFQDN, zoneID, zoneName)
128 if err != nil {
129 return err
130 }
131
132 for _, rec := range records {
133 _, err := d.client.Records.Delete(zoneID, rec.ID)
134 if err != nil {
135 return err
136 }
137 }
138
139 return nil
140 }
141
142 // Timeout returns the timeout and interval to use when checking for DNS propagation.
143 // Adjusting here to cope with spikes in propagation times.
144 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
145 return d.config.PropagationTimeout, d.config.PollingInterval
146 }
147
148 func (d *DNSProvider) getHostedZone(domain string) (string, string, error) {
149 zones, _, err := d.client.Domains.List()
150 if err != nil {
151 return "", "", fmt.Errorf("API call failed: %w", err)
152 }
153
154 authZone, err := dns01.FindZoneByFqdn(domain)
155 if err != nil {
156 return "", "", fmt.Errorf("could not find zone: %w", err)
157 }
158
159 var hostedZone dnspod.Domain
160
161 for _, zone := range zones {
162 if zone.Name == dns01.UnFqdn(authZone) {
163 hostedZone = zone
164 }
165 }
166
167 if hostedZone.ID == "" || hostedZone.ID == "0" {
168 return "", "", fmt.Errorf("zone %s not found for domain %s", authZone, domain)
169 }
170
171 return hostedZone.ID.String(), hostedZone.Name, nil
172 }
173
174 func (d *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) (*dnspod.Record, error) {
175 subDomain, err := dns01.ExtractSubDomain(fqdn, zone)
176 if err != nil {
177 return nil, err
178 }
179
180 return &dnspod.Record{
181 Type: "TXT",
182 Name: subDomain,
183 Value: value,
184 Line: "默认",
185 TTL: strconv.Itoa(ttl),
186 }, nil
187 }
188
189 func (d *DNSProvider) findTxtRecords(fqdn, zoneID, zoneName string) ([]dnspod.Record, error) {
190 subDomain, err := dns01.ExtractSubDomain(fqdn, zoneName)
191 if err != nil {
192 return nil, err
193 }
194
195 var records []dnspod.Record
196
197 result, _, err := d.client.Records.List(zoneID, subDomain)
198 if err != nil {
199 return records, fmt.Errorf("API call has failed: %w", err)
200 }
201
202 for _, record := range result {
203 if record.Name == subDomain {
204 records = append(records, record)
205 }
206 }
207
208 return records, nil
209 }
210