tencentcloud.go raw
1 // Package tencentcloud implements a DNS provider for solving the DNS-01 challenge using Tencent Cloud DNS.
2 package tencentcloud
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "math"
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 dnspod "github.com/go-acme/tencentclouddnspod/v20210323"
15 "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
16 "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
17 )
18
19 // Environment variables names.
20 const (
21 envNamespace = "TENCENTCLOUD_"
22
23 EnvSecretID = envNamespace + "SECRET_ID"
24 EnvSecretKey = envNamespace + "SECRET_KEY"
25 EnvRegion = envNamespace + "REGION"
26 EnvSessionToken = envNamespace + "SESSION_TOKEN"
27
28 EnvTTL = envNamespace + "TTL"
29 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
30 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
31 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
32 )
33
34 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
35
36 // Config is used to configure the creation of the DNSProvider.
37 type Config struct {
38 SecretID string
39 SecretKey string
40 Region string
41 SessionToken string
42
43 PropagationTimeout time.Duration
44 PollingInterval time.Duration
45 TTL int
46 HTTPTimeout time.Duration
47 }
48
49 // NewDefaultConfig returns a default configuration for the DNSProvider.
50 func NewDefaultConfig() *Config {
51 return &Config{
52 TTL: env.GetOrDefaultInt(EnvTTL, 600),
53 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
54 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
55 HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
56 }
57 }
58
59 // DNSProvider implements the challenge.Provider interface.
60 type DNSProvider struct {
61 config *Config
62 client *dnspod.Client
63 }
64
65 // NewDNSProvider returns a DNSProvider instance configured for Tencent Cloud DNS.
66 // Credentials must be passed in the environment variable: TENCENTCLOUD_SECRET_ID, TENCENTCLOUD_SECRET_KEY.
67 func NewDNSProvider() (*DNSProvider, error) {
68 values, err := env.Get(EnvSecretID, EnvSecretKey)
69 if err != nil {
70 return nil, fmt.Errorf("tencentcloud: %w", err)
71 }
72
73 config := NewDefaultConfig()
74 config.SecretID = values[EnvSecretID]
75 config.SecretKey = values[EnvSecretKey]
76 config.Region = env.GetOrDefaultString(EnvRegion, "")
77 config.SessionToken = env.GetOrDefaultString(EnvSessionToken, "")
78
79 return NewDNSProviderConfig(config)
80 }
81
82 // NewDNSProviderConfig return a DNSProvider instance configured for Tencent Cloud DNS.
83 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
84 if config == nil {
85 return nil, errors.New("tencentcloud: the configuration of the DNS provider is nil")
86 }
87
88 var credential *common.Credential
89
90 switch {
91 case config.SecretID != "" && config.SecretKey != "" && config.SessionToken != "":
92 credential = common.NewTokenCredential(config.SecretID, config.SecretKey, config.SessionToken)
93 case config.SecretID != "" && config.SecretKey != "":
94 credential = common.NewCredential(config.SecretID, config.SecretKey)
95 default:
96 return nil, errors.New("tencentcloud: credentials missing")
97 }
98
99 cpf := profile.NewClientProfile()
100 cpf.HttpProfile.Endpoint = "dnspod.tencentcloudapi.com"
101 cpf.HttpProfile.ReqTimeout = int(math.Round(config.HTTPTimeout.Seconds()))
102
103 client, err := dnspod.NewClient(credential, config.Region, cpf)
104 if err != nil {
105 return nil, fmt.Errorf("tencentcloud: %w", err)
106 }
107
108 return &DNSProvider{config: config, client: client}, nil
109 }
110
111 // Timeout returns the timeout and interval to use when checking for DNS propagation.
112 // Adjusting here to cope with spikes in propagation times.
113 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
114 return d.config.PropagationTimeout, d.config.PollingInterval
115 }
116
117 // Present creates a TXT record to fulfill the dns-01 challenge.
118 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
119 info := dns01.GetChallengeInfo(domain, keyAuth)
120
121 ctx := context.Background()
122
123 zone, err := d.getHostedZone(ctx, info.EffectiveFQDN)
124 if err != nil {
125 return fmt.Errorf("tencentcloud: failed to get hosted zone: %w", err)
126 }
127
128 recordName, err := extractRecordName(info.EffectiveFQDN, *zone.Name)
129 if err != nil {
130 return fmt.Errorf("tencentcloud: failed to extract record name: %w", err)
131 }
132
133 request := dnspod.NewCreateRecordRequest()
134 request.Domain = zone.Name
135 request.DomainId = zone.DomainId
136 request.SubDomain = common.StringPtr(recordName)
137 request.RecordType = common.StringPtr("TXT")
138 request.RecordLine = common.StringPtr("默认")
139 request.Value = common.StringPtr(info.Value)
140 request.TTL = common.Uint64Ptr(uint64(d.config.TTL))
141
142 _, err = dnspod.CreateRecordWithContext(ctx, d.client, request)
143 if err != nil {
144 return fmt.Errorf("dnspod: API call failed: %w", err)
145 }
146
147 return nil
148 }
149
150 // CleanUp removes the TXT record matching the specified parameters.
151 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
152 info := dns01.GetChallengeInfo(domain, keyAuth)
153
154 ctx := context.Background()
155
156 zone, err := d.getHostedZone(ctx, info.EffectiveFQDN)
157 if err != nil {
158 return fmt.Errorf("tencentcloud: failed to get hosted zone: %w", err)
159 }
160
161 records, err := d.findTxtRecords(ctx, zone, info.EffectiveFQDN)
162 if err != nil {
163 return fmt.Errorf("tencentcloud: failed to find TXT records: %w", err)
164 }
165
166 for _, record := range records {
167 request := dnspod.NewDeleteRecordRequest()
168 request.Domain = zone.Name
169 request.DomainId = zone.DomainId
170 request.RecordId = record.RecordId
171
172 _, err := dnspod.DeleteRecordWithContext(ctx, d.client, request)
173 if err != nil {
174 return fmt.Errorf("tencentcloud: delete record failed: %w", err)
175 }
176 }
177
178 return nil
179 }
180