Skip to content

Commit ea8aca4

Browse files
authored
Add DNS provider for AlibabaCloud ESA (#2703)
1 parent a8226a6 commit ea8aca4

File tree

10 files changed

+586
-43
lines changed

10 files changed

+586
-43
lines changed

README.md

Lines changed: 42 additions & 42 deletions
Large diffs are not rendered by default.

cmd/zz_gen_cmd_dnshelp.go

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/content/dns/zz_gen_aliesa.md

Lines changed: 78 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/data/zz_cli_help.toml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ require (
3131
github.com/dnsimple/dnsimple-go/v4 v4.0.0
3232
github.com/exoscale/egoscale/v3 v3.1.27
3333
github.com/go-acme/alidns-20150109/v4 v4.6.1
34+
github.com/go-acme/esa-20240910/v2 v2.40.1
3435
github.com/go-acme/tencentclouddnspod v1.1.10
3536
github.com/go-acme/tencentedgdeone v1.1.48
3637
github.com/go-jose/go-jose/v4 v4.1.3

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
314314
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
315315
github.com/go-acme/alidns-20150109/v4 v4.6.1 h1:Dch3aWRcw4U62+jKPjPQN3iW3TPvgIywATbvHzojXeo=
316316
github.com/go-acme/alidns-20150109/v4 v4.6.1/go.mod h1:RBcqBA5IvUWtlpjx6dC6EkPVyBNLQ+mR18XoaP38BFY=
317+
github.com/go-acme/esa-20240910/v2 v2.40.1 h1:pog3UlF5d+3LPoo1L8u8PqB189recIXX8T7pGoEz18A=
318+
github.com/go-acme/esa-20240910/v2 v2.40.1/go.mod h1:ZYdN9EN9ikn26SNapxCVjZ65pHT/1qm4fzuJ7QGVX6g=
317319
github.com/go-acme/tencentclouddnspod v1.1.10 h1:ERVJ4mc3cT4Nb3+n6H/c1AwZnChGBqLoymE0NVYscKI=
318320
github.com/go-acme/tencentclouddnspod v1.1.10/go.mod h1:Bo/0YQJ/99FM+44HmCQkByuptX1tJsJ9V14MGV/2Qco=
319321
github.com/go-acme/tencentedgdeone v1.1.48 h1:WLyLBsRVhSLFmtbEFXk0naLODSQn7X6J0Fc/qR8xVUk=

providers/dns/aliesa/aliesa.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// Package aliesa implements a DNS provider for solving the DNS-01 challenge using AlibabaCloud ESA.
2+
package aliesa
3+
4+
import (
5+
"context"
6+
"errors"
7+
"fmt"
8+
"sync"
9+
"time"
10+
11+
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
12+
"github.com/alibabacloud-go/tea/dara"
13+
"github.com/aliyun/credentials-go/credentials"
14+
esa "github.com/go-acme/esa-20240910/v2/client"
15+
"github.com/go-acme/lego/v4/challenge/dns01"
16+
"github.com/go-acme/lego/v4/platform/config/env"
17+
"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
18+
)
19+
20+
// Environment variables names.
21+
const (
22+
envNamespace = "ALIESA_"
23+
24+
EnvRAMRole = envNamespace + "RAM_ROLE"
25+
EnvAccessKey = envNamespace + "ACCESS_KEY"
26+
EnvSecretKey = envNamespace + "SECRET_KEY"
27+
EnvSecurityToken = envNamespace + "SECURITY_TOKEN"
28+
EnvRegionID = envNamespace + "REGION_ID"
29+
30+
EnvTTL = envNamespace + "TTL"
31+
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
32+
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
33+
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
34+
)
35+
36+
const defaultRegionID = "cn-hangzhou"
37+
38+
// Config is used to configure the creation of the DNSProvider.
39+
type Config struct {
40+
RAMRole string
41+
APIKey string
42+
SecretKey string
43+
SecurityToken string
44+
RegionID 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, dns01.DefaultTTL),
56+
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
57+
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
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 *esa.Client
66+
67+
recordIDs map[string]int64
68+
recordIDsMu sync.Mutex
69+
}
70+
71+
// NewDNSProvider returns a DNSProvider instance configured for AlibabaCloud ESA.
72+
func NewDNSProvider() (*DNSProvider, error) {
73+
config := NewDefaultConfig()
74+
config.RegionID = env.GetOrFile(EnvRegionID)
75+
76+
values, err := env.Get(EnvRAMRole)
77+
if err == nil {
78+
config.RAMRole = values[EnvRAMRole]
79+
return NewDNSProviderConfig(config)
80+
}
81+
82+
values, err = env.Get(EnvAccessKey, EnvSecretKey)
83+
if err != nil {
84+
return nil, fmt.Errorf("aliesa: %w", err)
85+
}
86+
87+
config.APIKey = values[EnvAccessKey]
88+
config.SecretKey = values[EnvSecretKey]
89+
config.SecurityToken = env.GetOrFile(EnvSecurityToken)
90+
91+
return NewDNSProviderConfig(config)
92+
}
93+
94+
// NewDNSProviderConfig return a DNSProvider instance configured for AlibabaCloud ESA.
95+
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
96+
if config == nil {
97+
return nil, errors.New("aliesa: the configuration of the DNS provider is nil")
98+
}
99+
100+
if config.RegionID == "" {
101+
config.RegionID = defaultRegionID
102+
}
103+
104+
cfg := new(openapi.Config).
105+
SetRegionId(config.RegionID).
106+
SetReadTimeout(int(config.HTTPTimeout.Milliseconds()))
107+
108+
switch {
109+
case config.RAMRole != "":
110+
// https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance
111+
credentialsCfg := new(credentials.Config).
112+
SetType("ecs_ram_role").
113+
SetRoleName(config.RAMRole)
114+
115+
credentialClient, err := credentials.NewCredential(credentialsCfg)
116+
if err != nil {
117+
return nil, fmt.Errorf("aliesa: new credential: %w", err)
118+
}
119+
120+
cfg = cfg.SetCredential(credentialClient)
121+
122+
case config.APIKey != "" && config.SecretKey != "" && config.SecurityToken != "":
123+
cfg = cfg.
124+
SetAccessKeyId(config.APIKey).
125+
SetAccessKeySecret(config.SecretKey).
126+
SetSecurityToken(config.SecurityToken)
127+
128+
case config.APIKey != "" && config.SecretKey != "":
129+
cfg = cfg.
130+
SetAccessKeyId(config.APIKey).
131+
SetAccessKeySecret(config.SecretKey)
132+
133+
default:
134+
return nil, errors.New("aliesa: ram role or credentials missing")
135+
}
136+
137+
client, err := esa.NewClient(cfg)
138+
if err != nil {
139+
return nil, fmt.Errorf("aliesa: new client: %w", err)
140+
}
141+
142+
// Workaround to get a regional URL.
143+
// https://github.com/alibabacloud-go/esa-20240910/blame/7660e3aab2045d4820e4b83427a154efe0c79319/client/client.go#L27
144+
// The `EndpointRule` is hardcoded with an empty string, so the region is ignored.
145+
client.Endpoint = nil
146+
client.EndpointRule = ptr.Pointer("regional")
147+
148+
client.Endpoint, err = esa.GetEndpoint(client, dara.String("esa"), client.RegionId, client.EndpointRule, client.Network, client.Suffix, client.EndpointMap, client.Endpoint)
149+
if err != nil {
150+
return nil, fmt.Errorf("aliesa: get endpoint: %w", err)
151+
}
152+
153+
return &DNSProvider{
154+
config: config,
155+
client: client,
156+
recordIDs: make(map[string]int64),
157+
}, nil
158+
}
159+
160+
// Present creates a TXT record using the specified parameters.
161+
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
162+
ctx := context.Background()
163+
164+
info := dns01.GetChallengeInfo(domain, keyAuth)
165+
166+
siteID, err := d.getSiteID(ctx, info.EffectiveFQDN)
167+
if err != nil {
168+
return fmt.Errorf("aliesa: %w", err)
169+
}
170+
171+
crReq := new(esa.CreateRecordRequest).
172+
SetSiteId(siteID).
173+
SetType("TXT").
174+
SetRecordName(dns01.UnFqdn(info.EffectiveFQDN)).
175+
SetTtl(int32(d.config.TTL)).
176+
SetData(new(esa.CreateRecordRequestData).SetValue(info.Value))
177+
178+
// https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord
179+
crResp, err := esa.CreateRecordWithContext(ctx, d.client, crReq, &dara.RuntimeOptions{})
180+
if err != nil {
181+
return fmt.Errorf("aliesa: create record: %w", err)
182+
}
183+
184+
d.recordIDsMu.Lock()
185+
d.recordIDs[token] = ptr.Deref(crResp.Body.GetRecordId())
186+
d.recordIDsMu.Unlock()
187+
188+
return nil
189+
}
190+
191+
// CleanUp removes the TXT record matching the specified parameters.
192+
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
193+
ctx := context.Background()
194+
195+
info := dns01.GetChallengeInfo(domain, keyAuth)
196+
197+
// gets the record's unique ID
198+
d.recordIDsMu.Lock()
199+
recordID, ok := d.recordIDs[token]
200+
d.recordIDsMu.Unlock()
201+
202+
if !ok {
203+
return fmt.Errorf("aliesa: unknown record ID for '%s'", info.EffectiveFQDN)
204+
}
205+
206+
drReq := new(esa.DeleteRecordRequest).
207+
SetRecordId(recordID)
208+
209+
// https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-deleterecord
210+
_, err := esa.DeleteRecordWithContext(ctx, d.client, drReq, &dara.RuntimeOptions{})
211+
if err != nil {
212+
return fmt.Errorf("aliesa: delete record: %w", err)
213+
}
214+
215+
return nil
216+
}
217+
218+
// Timeout returns the timeout and interval to use when checking for DNS propagation.
219+
// Adjusting here to cope with spikes in propagation times.
220+
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
221+
return d.config.PropagationTimeout, d.config.PollingInterval
222+
}
223+
224+
func (d *DNSProvider) getSiteID(ctx context.Context, fqdn string) (int64, error) {
225+
authZone, err := dns01.FindZoneByFqdn(fqdn)
226+
if err != nil {
227+
return 0, fmt.Errorf("aliesa: could not find zone for domain %q: %w", fqdn, err)
228+
}
229+
230+
lsReq := new(esa.ListSitesRequest).
231+
SetSiteName(dns01.UnFqdn(authZone)).
232+
SetSiteSearchType("suffix")
233+
234+
// https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-listsites
235+
lsResp, err := esa.ListSitesWithContext(ctx, d.client, lsReq, &dara.RuntimeOptions{})
236+
if err != nil {
237+
return 0, fmt.Errorf("list sites: %w", err)
238+
}
239+
240+
for f := range dns01.UnFqdnDomainsSeq(fqdn) {
241+
domain := dns01.UnFqdn(f)
242+
243+
for _, site := range lsResp.Body.GetSites() {
244+
if ptr.Deref(site.GetSiteName()) == domain {
245+
return ptr.Deref(site.GetSiteId()), nil
246+
}
247+
}
248+
}
249+
250+
return 0, fmt.Errorf("site not found (fqdn: %q)", fqdn)
251+
}

0 commit comments

Comments
 (0)