Skip to content

Commit f77681b

Browse files
authored
feat(ssl证书): 实现华为云dns (#6407)
1 parent 535d4bb commit f77681b

File tree

6 files changed

+536
-0
lines changed

6 files changed

+536
-0
lines changed

backend/utils/ssl/client.go

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ssl
33
import (
44
"crypto"
55
"encoding/json"
6+
"github.com/1Panel-dev/1Panel/backend/utils/ssl/huaweicloud"
67
"os"
78
"strings"
89
"time"
@@ -72,6 +73,7 @@ const (
7273
NameCom DnsType = "NameCom"
7374
Godaddy DnsType = "Godaddy"
7475
TencentCloud DnsType = "TencentCloud"
76+
HuaweiCloud DnsType = "HuaweiCloud"
7577
)
7678

7779
type DNSParam struct {
@@ -84,6 +86,7 @@ type DNSParam struct {
8486
APIUser string `json:"apiUser"`
8587
APISecret string `json:"apiSecret"`
8688
SecretID string `json:"secretID"`
89+
Region string `json:"region"`
8790
}
8891

8992
var (
@@ -166,6 +169,15 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
166169
tencentCloudConfig.PollingInterval = pollingInterval
167170
tencentCloudConfig.TTL = ttl
168171
p, err = tencentcloud.NewDNSProviderConfig(tencentCloudConfig)
172+
case HuaweiCloud:
173+
huaweiCloudConfig := huaweicloud.NewDefaultConfig()
174+
huaweiCloudConfig.AccessKeyID = param.AccessKey
175+
huaweiCloudConfig.SecretAccessKey = param.SecretKey
176+
huaweiCloudConfig.Region = param.Region
177+
huaweiCloudConfig.PropagationTimeout = propagationTimeout
178+
huaweiCloudConfig.PollingInterval = pollingInterval
179+
huaweiCloudConfig.TTL = int32(ttl)
180+
p, err = huaweicloud.NewDNSProviderConfig(huaweiCloudConfig)
169181
}
170182
if err != nil {
171183
return err
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
// Package huaweicloud implements a DNS provider for solving the DNS-01 challenge using Huawei Cloud.
2+
package huaweicloud
3+
4+
import (
5+
"errors"
6+
"fmt"
7+
"strconv"
8+
"strings"
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/platform/wait"
15+
hwauthbasic "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
16+
hwconfig "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/config"
17+
hwdns "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2"
18+
hwmodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model"
19+
hwregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/region"
20+
)
21+
22+
// Environment variables names.
23+
const (
24+
envNamespace = "HUAWEICLOUD_"
25+
26+
EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID"
27+
EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY"
28+
EnvRegion = envNamespace + "REGION"
29+
30+
EnvTTL = envNamespace + "TTL"
31+
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
32+
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
33+
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
34+
)
35+
36+
// Config is used to configure the creation of the DNSProvider.
37+
type Config struct {
38+
AccessKeyID string
39+
SecretAccessKey string
40+
Region string
41+
42+
PropagationTimeout time.Duration
43+
PollingInterval time.Duration
44+
TTL int32
45+
HTTPTimeout time.Duration
46+
}
47+
48+
// NewDefaultConfig returns a default configuration for the DNSProvider.
49+
func NewDefaultConfig() *Config {
50+
return &Config{
51+
TTL: int32(env.GetOrDefaultInt(EnvTTL, 300)),
52+
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
53+
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
54+
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
55+
}
56+
}
57+
58+
// DNSProvider implements the challenge.Provider interface.
59+
type DNSProvider struct {
60+
config *Config
61+
client *hwdns.DnsClient
62+
63+
recordIDs map[string]string
64+
recordIDsMu sync.Mutex
65+
}
66+
67+
// NewDNSProvider returns a DNSProvider instance configured for Huawei Cloud.
68+
// Credentials must be passed in the environment variables:
69+
// HUAWEICLOUD_ACCESS_KEY_ID, HUAWEICLOUD_SECRET_ACCESS_KEY, and HUAWEICLOUD_REGION.
70+
func NewDNSProvider() (*DNSProvider, error) {
71+
values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion)
72+
if err != nil {
73+
return nil, fmt.Errorf("huaweicloud: %w", err)
74+
}
75+
76+
config := NewDefaultConfig()
77+
config.AccessKeyID = values[EnvAccessKeyID]
78+
config.SecretAccessKey = values[EnvSecretAccessKey]
79+
config.Region = values[EnvRegion]
80+
81+
return NewDNSProviderConfig(config)
82+
}
83+
84+
// NewDNSProviderConfig return a DNSProvider instance configured for Huawei Cloud.
85+
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
86+
if config == nil {
87+
return nil, errors.New("huaweicloud: the configuration of the DNS provider is nil")
88+
}
89+
90+
if config.AccessKeyID == "" || config.SecretAccessKey == "" || config.Region == "" {
91+
return nil, errors.New("huaweicloud: credentials missing")
92+
}
93+
94+
auth, err := hwauthbasic.NewCredentialsBuilder().
95+
WithAk(config.AccessKeyID).
96+
WithSk(config.SecretAccessKey).
97+
SafeBuild()
98+
if err != nil {
99+
return nil, fmt.Errorf("huaweicloud: crendential build: %w", err)
100+
}
101+
102+
region, err := hwregion.SafeValueOf(config.Region)
103+
if err != nil {
104+
return nil, fmt.Errorf("huaweicloud: safe region: %w", err)
105+
}
106+
107+
client, err := hwdns.DnsClientBuilder().
108+
WithHttpConfig(hwconfig.DefaultHttpConfig().WithTimeout(config.HTTPTimeout)).
109+
WithRegion(region).
110+
WithCredential(auth).
111+
SafeBuild()
112+
if err != nil {
113+
return nil, fmt.Errorf("huaweicloud: client build: %w", err)
114+
}
115+
116+
return &DNSProvider{
117+
config: config,
118+
client: hwdns.NewDnsClient(client),
119+
recordIDs: map[string]string{},
120+
}, nil
121+
}
122+
123+
// Present creates a TXT record using the specified parameters.
124+
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
125+
info := dns01.GetChallengeInfo(domain, keyAuth)
126+
127+
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
128+
if err != nil {
129+
return fmt.Errorf("huaweicloud: could not find zone for domain %q: %w", domain, err)
130+
}
131+
132+
zoneID, err := d.getZoneID(authZone)
133+
if err != nil {
134+
return fmt.Errorf("huaweicloud: %w", err)
135+
}
136+
137+
recordSetID, err := d.getOrCreateRecordSetID(domain, zoneID, info)
138+
if err != nil {
139+
return fmt.Errorf("huaweicloud: %w", err)
140+
}
141+
142+
d.recordIDsMu.Lock()
143+
d.recordIDs[token] = recordSetID
144+
d.recordIDsMu.Unlock()
145+
146+
err = wait.For("record set sync on "+domain, d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) {
147+
rs, errShow := d.client.ShowRecordSet(&hwmodel.ShowRecordSetRequest{
148+
ZoneId: zoneID,
149+
RecordsetId: recordSetID,
150+
})
151+
if errShow != nil {
152+
return false, fmt.Errorf("show record set: %w", errShow)
153+
}
154+
155+
return !strings.HasSuffix(deref(rs.Status), "PENDING_"), nil
156+
})
157+
if err != nil {
158+
return fmt.Errorf("huaweicloud: %w", err)
159+
}
160+
161+
return nil
162+
}
163+
164+
// CleanUp removes the TXT record matching the specified parameters.
165+
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
166+
info := dns01.GetChallengeInfo(domain, keyAuth)
167+
168+
// gets the record's unique ID from when we created it
169+
d.recordIDsMu.Lock()
170+
recordID, ok := d.recordIDs[token]
171+
d.recordIDsMu.Unlock()
172+
if !ok {
173+
return fmt.Errorf("huaweicloud: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token)
174+
}
175+
176+
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
177+
if err != nil {
178+
return fmt.Errorf("huaweicloud: could not find zone for domain %q: %w", domain, err)
179+
}
180+
181+
zoneID, err := d.getZoneID(authZone)
182+
if err != nil {
183+
return fmt.Errorf("huaweicloud: %w", err)
184+
}
185+
186+
request := &hwmodel.DeleteRecordSetRequest{
187+
ZoneId: zoneID,
188+
RecordsetId: recordID,
189+
}
190+
191+
_, err = d.client.DeleteRecordSet(request)
192+
if err != nil {
193+
return fmt.Errorf("huaweicloud: delete record: %w", err)
194+
}
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+
205+
func (d *DNSProvider) getOrCreateRecordSetID(domain, zoneID string, info dns01.ChallengeInfo) (string, error) {
206+
records, err := d.client.ListRecordSetsByZone(&hwmodel.ListRecordSetsByZoneRequest{
207+
ZoneId: zoneID,
208+
Name: pointer(info.EffectiveFQDN),
209+
})
210+
if err != nil {
211+
return "", fmt.Errorf("record list: unable to get record %s for zone %s: %w", info.EffectiveFQDN, domain, err)
212+
}
213+
214+
var existingRecordSet *hwmodel.ListRecordSets
215+
216+
for _, record := range deref(records.Recordsets) {
217+
if deref(record.Type) == "TXT" && deref(record.Name) == info.EffectiveFQDN {
218+
existingRecordSet = &record
219+
}
220+
}
221+
222+
value := strconv.Quote(info.Value)
223+
224+
if existingRecordSet == nil {
225+
request := &hwmodel.CreateRecordSetRequest{
226+
ZoneId: zoneID,
227+
Body: &hwmodel.CreateRecordSetRequestBody{
228+
Name: info.EffectiveFQDN,
229+
Description: pointer("Added TXT record for ACME dns-01 challenge using lego client"),
230+
Type: "TXT",
231+
Ttl: pointer(d.config.TTL),
232+
Records: []string{value},
233+
},
234+
}
235+
236+
resp, errCreate := d.client.CreateRecordSet(request)
237+
if errCreate != nil {
238+
return "", fmt.Errorf("create record set: %w", errCreate)
239+
}
240+
241+
return deref(resp.Id), nil
242+
}
243+
244+
updateRequest := &hwmodel.UpdateRecordSetRequest{
245+
ZoneId: zoneID,
246+
RecordsetId: deref(existingRecordSet.Id),
247+
Body: &hwmodel.UpdateRecordSetReq{
248+
Name: existingRecordSet.Name,
249+
Description: existingRecordSet.Description,
250+
Type: existingRecordSet.Type,
251+
Ttl: existingRecordSet.Ttl,
252+
Records: pointer(append(deref(existingRecordSet.Records), value)),
253+
},
254+
}
255+
256+
resp, err := d.client.UpdateRecordSet(updateRequest)
257+
if err != nil {
258+
return "", fmt.Errorf("update record set: %w", err)
259+
}
260+
261+
return deref(resp.Id), nil
262+
}
263+
264+
func (d *DNSProvider) getZoneID(authZone string) (string, error) {
265+
zones, err := d.client.ListPublicZones(&hwmodel.ListPublicZonesRequest{})
266+
if err != nil {
267+
return "", fmt.Errorf("unable to get zone: %w", err)
268+
}
269+
270+
for _, zone := range deref(zones.Zones) {
271+
if deref(zone.Name) == authZone {
272+
return deref(zone.Id), nil
273+
}
274+
}
275+
276+
return "", fmt.Errorf("zone %q not found", authZone)
277+
}
278+
279+
func pointer[T any](v T) *T { return &v }
280+
281+
func deref[T any](v *T) T {
282+
if v == nil {
283+
var zero T
284+
return zero
285+
}
286+
287+
return *v
288+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
Name = "Huawei Cloud"
2+
Description = ''''''
3+
URL = "https://huaweicloud.com"
4+
Code = "huaweicloud"
5+
Since = "v4.19"
6+
7+
Example = '''
8+
HUAWEICLOUD_ACCESS_KEY_ID=your-access-key-id \
9+
HUAWEICLOUD_SECRET_ACCESS_KEY=your-secret-access-key \
10+
HUAWEICLOUD_REGION=cn-south-1 \
11+
lego --email [email protected] --dns huaweicloud --domains my.example.org run
12+
'''
13+
14+
[Configuration]
15+
[Configuration.Credentials]
16+
HUAWEICLOUD_ACCESS_KEY_ID = "Access key ID"
17+
HUAWEICLOUD_SECRET_ACCESS_KEY = "Access Key secret"
18+
HUAWEICLOUD_REGION = "Region"
19+
20+
[Configuration.Additional]
21+
HUAWEICLOUD_POLLING_INTERVAL = "Time between DNS propagation check"
22+
HUAWEICLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
23+
HUAWEICLOUD_TTL = "The TTL of the TXT record used for the DNS challenge"
24+
HUAWEICLOUD_HTTP_TIMEOUT = "API request timeout"
25+
26+
[Links]
27+
API = "https://console-intl.huaweicloud.com/apiexplorer/#/openapi/DNS/doc?locale=en-us"
28+
CN_API = "https://support.huaweicloud.com/api-dns/zh-cn_topic_0132421999.html"
29+
GoClient = "https://github.com/huaweicloud/huaweicloud-sdk-go-v3"

0 commit comments

Comments
 (0)