edgeone.go raw
1 // Package edgeone implements a DNS provider for solving the DNS-01 challenge using Tencent EdgeOne.
2 package edgeone
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "math"
9 "sync"
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/ptr"
15 teo "github.com/go-acme/tencentedgdeone/v20220901"
16 "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
17 "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
18 "golang.org/x/net/idna"
19 )
20
21 // Environment variables names.
22 const (
23 envNamespace = "EDGEONE_"
24
25 EnvSecretID = envNamespace + "SECRET_ID"
26 EnvSecretKey = envNamespace + "SECRET_KEY"
27 EnvRegion = envNamespace + "REGION"
28 EnvSessionToken = envNamespace + "SESSION_TOKEN"
29 EnvZonesMapping = envNamespace + "ZONES_MAPPING"
30
31 EnvTTL = envNamespace + "TTL"
32 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
33 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
34 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
35 )
36
37 // Config is used to configure the creation of the DNSProvider.
38 type Config struct {
39 SecretID string
40 SecretKey string
41 Region string
42 SessionToken string
43
44 ZonesMapping map[string]string
45
46 PropagationTimeout time.Duration
47 PollingInterval time.Duration
48 TTL int
49 HTTPTimeout time.Duration
50 }
51
52 // NewDefaultConfig returns a default configuration for the DNSProvider.
53 func NewDefaultConfig() *Config {
54 return &Config{
55 TTL: env.GetOrDefaultInt(EnvTTL, 60),
56 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 20*time.Minute),
57 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 30*time.Second),
58 HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
59 }
60 }
61
62 // DNSProvider implements the challenge.Provider interface.
63 type DNSProvider struct {
64 config *Config
65 client *teo.Client
66
67 recordIDs map[string]*string
68 recordIDsMu sync.Mutex
69 }
70
71 // NewDNSProvider returns a DNSProvider instance configured for Tencent EdgeOne.
72 func NewDNSProvider() (*DNSProvider, error) {
73 values, err := env.Get(EnvSecretID, EnvSecretKey)
74 if err != nil {
75 return nil, fmt.Errorf("edgeone: %w", err)
76 }
77
78 config := NewDefaultConfig()
79 config.SecretID = values[EnvSecretID]
80 config.SecretKey = values[EnvSecretKey]
81 config.Region = env.GetOrDefaultString(EnvRegion, "")
82 config.SessionToken = env.GetOrDefaultString(EnvSessionToken, "")
83
84 mapping := env.GetOrDefaultString(EnvZonesMapping, "")
85 if mapping != "" {
86 config.ZonesMapping, err = env.ParsePairs(mapping)
87 if err != nil {
88 return nil, fmt.Errorf("edgeone: zones mapping: %w", err)
89 }
90 }
91
92 return NewDNSProviderConfig(config)
93 }
94
95 // NewDNSProviderConfig return a DNSProvider instance configured for Tencent EdgeOne.
96 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
97 if config == nil {
98 return nil, errors.New("edgeone: the configuration of the DNS provider is nil")
99 }
100
101 var credential *common.Credential
102
103 switch {
104 case config.SecretID != "" && config.SecretKey != "" && config.SessionToken != "":
105 credential = common.NewTokenCredential(config.SecretID, config.SecretKey, config.SessionToken)
106 case config.SecretID != "" && config.SecretKey != "":
107 credential = common.NewCredential(config.SecretID, config.SecretKey)
108 default:
109 return nil, errors.New("edgeone: credentials missing")
110 }
111
112 cpf := profile.NewClientProfile()
113 cpf.HttpProfile.Endpoint = "teo.intl.tencentcloudapi.com"
114 cpf.HttpProfile.ReqTimeout = int(math.Round(config.HTTPTimeout.Seconds()))
115
116 client, err := teo.NewClient(credential, config.Region, cpf)
117 if err != nil {
118 return nil, fmt.Errorf("edgeone: %w", err)
119 }
120
121 return &DNSProvider{
122 config: config,
123 client: client,
124 recordIDs: map[string]*string{},
125 }, nil
126 }
127
128 // Present creates a TXT record using the specified parameters.
129 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
130 info := dns01.GetChallengeInfo(domain, keyAuth)
131
132 ctx := context.Background()
133
134 zoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)
135 if err != nil {
136 return fmt.Errorf("edgeone: failed to get hosted zone: %w", err)
137 }
138
139 punnyCoded, err := idna.ToASCII(dns01.UnFqdn(info.EffectiveFQDN))
140 if err != nil {
141 return fmt.Errorf("edgeone: fail to convert punycode: %w", err)
142 }
143
144 request := teo.NewCreateDnsRecordRequest()
145 request.Name = ptr.Pointer(punnyCoded)
146 request.ZoneId = zoneID
147 request.Type = ptr.Pointer("TXT")
148 request.Content = ptr.Pointer(info.Value)
149 request.TTL = ptr.Pointer(int64(d.config.TTL))
150
151 nr, err := teo.CreateDnsRecordWithContext(ctx, d.client, request)
152 if err != nil {
153 return fmt.Errorf("edgeone: API call failed: %w", err)
154 }
155
156 d.recordIDsMu.Lock()
157 d.recordIDs[token] = nr.Response.RecordId
158 d.recordIDsMu.Unlock()
159
160 return nil
161 }
162
163 // CleanUp removes the TXT record matching the specified parameters.
164 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
165 info := dns01.GetChallengeInfo(domain, keyAuth)
166
167 ctx := context.Background()
168
169 zoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)
170 if err != nil {
171 return fmt.Errorf("edgeone: failed to get hosted zone: %w", err)
172 }
173
174 // get the record's unique ID from when we created it
175 d.recordIDsMu.Lock()
176 recordID, ok := d.recordIDs[token]
177 d.recordIDsMu.Unlock()
178
179 if !ok {
180 return fmt.Errorf("edgeone: unknown record ID for '%s'", info.EffectiveFQDN)
181 }
182
183 request := teo.NewDeleteDnsRecordsRequest()
184 request.ZoneId = zoneID
185 request.RecordIds = []*string{recordID}
186
187 _, err = teo.DeleteDnsRecordsWithContext(ctx, d.client, request)
188 if err != nil {
189 return fmt.Errorf("edgeone: delete record failed: %w", err)
190 }
191
192 d.recordIDsMu.Lock()
193 delete(d.recordIDs, token)
194 d.recordIDsMu.Unlock()
195
196 return nil
197 }
198
199 // Timeout returns the timeout and interval to use when checking for DNS propagation.
200 // Adjusting here to cope with spikes in propagation times.
201 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
202 return d.config.PropagationTimeout, d.config.PollingInterval
203 }
204