Skip to content

Commit

Permalink
feat(ssl证书): 实现华为云dns (#6407)
Browse files Browse the repository at this point in the history
  • Loading branch information
endymx authored Sep 13, 2024
1 parent 535d4bb commit f77681b
Show file tree
Hide file tree
Showing 6 changed files with 536 additions and 0 deletions.
12 changes: 12 additions & 0 deletions backend/utils/ssl/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ssl
import (
"crypto"
"encoding/json"
"github.com/1Panel-dev/1Panel/backend/utils/ssl/huaweicloud"
"os"
"strings"
"time"
Expand Down Expand Up @@ -72,6 +73,7 @@ const (
NameCom DnsType = "NameCom"
Godaddy DnsType = "Godaddy"
TencentCloud DnsType = "TencentCloud"
HuaweiCloud DnsType = "HuaweiCloud"
)

type DNSParam struct {
Expand All @@ -84,6 +86,7 @@ type DNSParam struct {
APIUser string `json:"apiUser"`
APISecret string `json:"apiSecret"`
SecretID string `json:"secretID"`
Region string `json:"region"`
}

var (
Expand Down Expand Up @@ -166,6 +169,15 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
tencentCloudConfig.PollingInterval = pollingInterval
tencentCloudConfig.TTL = ttl
p, err = tencentcloud.NewDNSProviderConfig(tencentCloudConfig)
case HuaweiCloud:
huaweiCloudConfig := huaweicloud.NewDefaultConfig()
huaweiCloudConfig.AccessKeyID = param.AccessKey
huaweiCloudConfig.SecretAccessKey = param.SecretKey
huaweiCloudConfig.Region = param.Region
huaweiCloudConfig.PropagationTimeout = propagationTimeout
huaweiCloudConfig.PollingInterval = pollingInterval
huaweiCloudConfig.TTL = int32(ttl)
p, err = huaweicloud.NewDNSProviderConfig(huaweiCloudConfig)
}
if err != nil {
return err
Expand Down
288 changes: 288 additions & 0 deletions backend/utils/ssl/huaweicloud/huaweicloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
// Package huaweicloud implements a DNS provider for solving the DNS-01 challenge using Huawei Cloud.
package huaweicloud

import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"

"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/go-acme/lego/v4/platform/wait"
hwauthbasic "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
hwconfig "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/config"
hwdns "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2"
hwmodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model"
hwregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/region"
)

// Environment variables names.
const (
envNamespace = "HUAWEICLOUD_"

EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID"
EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY"
EnvRegion = envNamespace + "REGION"

EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)

// Config is used to configure the creation of the DNSProvider.
type Config struct {
AccessKeyID string
SecretAccessKey string
Region string

PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int32
HTTPTimeout time.Duration
}

// NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config {
return &Config{
TTL: int32(env.GetOrDefaultInt(EnvTTL, 300)),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}

// DNSProvider implements the challenge.Provider interface.
type DNSProvider struct {
config *Config
client *hwdns.DnsClient

recordIDs map[string]string
recordIDsMu sync.Mutex
}

// NewDNSProvider returns a DNSProvider instance configured for Huawei Cloud.
// Credentials must be passed in the environment variables:
// HUAWEICLOUD_ACCESS_KEY_ID, HUAWEICLOUD_SECRET_ACCESS_KEY, and HUAWEICLOUD_REGION.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion)
if err != nil {
return nil, fmt.Errorf("huaweicloud: %w", err)
}

config := NewDefaultConfig()
config.AccessKeyID = values[EnvAccessKeyID]
config.SecretAccessKey = values[EnvSecretAccessKey]
config.Region = values[EnvRegion]

return NewDNSProviderConfig(config)
}

// NewDNSProviderConfig return a DNSProvider instance configured for Huawei Cloud.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("huaweicloud: the configuration of the DNS provider is nil")
}

if config.AccessKeyID == "" || config.SecretAccessKey == "" || config.Region == "" {
return nil, errors.New("huaweicloud: credentials missing")
}

auth, err := hwauthbasic.NewCredentialsBuilder().
WithAk(config.AccessKeyID).
WithSk(config.SecretAccessKey).
SafeBuild()
if err != nil {
return nil, fmt.Errorf("huaweicloud: crendential build: %w", err)
}

region, err := hwregion.SafeValueOf(config.Region)
if err != nil {
return nil, fmt.Errorf("huaweicloud: safe region: %w", err)
}

client, err := hwdns.DnsClientBuilder().
WithHttpConfig(hwconfig.DefaultHttpConfig().WithTimeout(config.HTTPTimeout)).
WithRegion(region).
WithCredential(auth).
SafeBuild()
if err != nil {
return nil, fmt.Errorf("huaweicloud: client build: %w", err)
}

return &DNSProvider{
config: config,
client: hwdns.NewDnsClient(client),
recordIDs: map[string]string{},
}, nil
}

// Present creates a TXT record using the specified parameters.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)

authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("huaweicloud: could not find zone for domain %q: %w", domain, err)
}

zoneID, err := d.getZoneID(authZone)
if err != nil {
return fmt.Errorf("huaweicloud: %w", err)
}

recordSetID, err := d.getOrCreateRecordSetID(domain, zoneID, info)
if err != nil {
return fmt.Errorf("huaweicloud: %w", err)
}

d.recordIDsMu.Lock()
d.recordIDs[token] = recordSetID
d.recordIDsMu.Unlock()

err = wait.For("record set sync on "+domain, d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) {
rs, errShow := d.client.ShowRecordSet(&hwmodel.ShowRecordSetRequest{
ZoneId: zoneID,
RecordsetId: recordSetID,
})
if errShow != nil {
return false, fmt.Errorf("show record set: %w", errShow)
}

return !strings.HasSuffix(deref(rs.Status), "PENDING_"), nil
})
if err != nil {
return fmt.Errorf("huaweicloud: %w", err)
}

return nil
}

// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)

// gets the record's unique ID from when we created it
d.recordIDsMu.Lock()
recordID, ok := d.recordIDs[token]
d.recordIDsMu.Unlock()
if !ok {
return fmt.Errorf("huaweicloud: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token)
}

authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("huaweicloud: could not find zone for domain %q: %w", domain, err)
}

zoneID, err := d.getZoneID(authZone)
if err != nil {
return fmt.Errorf("huaweicloud: %w", err)
}

request := &hwmodel.DeleteRecordSetRequest{
ZoneId: zoneID,
RecordsetId: recordID,
}

_, err = d.client.DeleteRecordSet(request)
if err != nil {
return fmt.Errorf("huaweicloud: delete record: %w", err)
}

return nil
}

// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}

func (d *DNSProvider) getOrCreateRecordSetID(domain, zoneID string, info dns01.ChallengeInfo) (string, error) {
records, err := d.client.ListRecordSetsByZone(&hwmodel.ListRecordSetsByZoneRequest{
ZoneId: zoneID,
Name: pointer(info.EffectiveFQDN),
})
if err != nil {
return "", fmt.Errorf("record list: unable to get record %s for zone %s: %w", info.EffectiveFQDN, domain, err)
}

var existingRecordSet *hwmodel.ListRecordSets

for _, record := range deref(records.Recordsets) {
if deref(record.Type) == "TXT" && deref(record.Name) == info.EffectiveFQDN {
existingRecordSet = &record
}
}

value := strconv.Quote(info.Value)

if existingRecordSet == nil {
request := &hwmodel.CreateRecordSetRequest{
ZoneId: zoneID,
Body: &hwmodel.CreateRecordSetRequestBody{
Name: info.EffectiveFQDN,
Description: pointer("Added TXT record for ACME dns-01 challenge using lego client"),
Type: "TXT",
Ttl: pointer(d.config.TTL),
Records: []string{value},
},
}

resp, errCreate := d.client.CreateRecordSet(request)
if errCreate != nil {
return "", fmt.Errorf("create record set: %w", errCreate)
}

return deref(resp.Id), nil
}

updateRequest := &hwmodel.UpdateRecordSetRequest{
ZoneId: zoneID,
RecordsetId: deref(existingRecordSet.Id),
Body: &hwmodel.UpdateRecordSetReq{
Name: existingRecordSet.Name,
Description: existingRecordSet.Description,
Type: existingRecordSet.Type,
Ttl: existingRecordSet.Ttl,
Records: pointer(append(deref(existingRecordSet.Records), value)),
},
}

resp, err := d.client.UpdateRecordSet(updateRequest)
if err != nil {
return "", fmt.Errorf("update record set: %w", err)
}

return deref(resp.Id), nil
}

func (d *DNSProvider) getZoneID(authZone string) (string, error) {
zones, err := d.client.ListPublicZones(&hwmodel.ListPublicZonesRequest{})
if err != nil {
return "", fmt.Errorf("unable to get zone: %w", err)
}

for _, zone := range deref(zones.Zones) {
if deref(zone.Name) == authZone {
return deref(zone.Id), nil
}
}

return "", fmt.Errorf("zone %q not found", authZone)
}

func pointer[T any](v T) *T { return &v }

func deref[T any](v *T) T {
if v == nil {
var zero T
return zero
}

return *v
}
29 changes: 29 additions & 0 deletions backend/utils/ssl/huaweicloud/huaweicloud.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Name = "Huawei Cloud"
Description = ''''''
URL = "https://huaweicloud.com"
Code = "huaweicloud"
Since = "v4.19"

Example = '''
HUAWEICLOUD_ACCESS_KEY_ID=your-access-key-id \
HUAWEICLOUD_SECRET_ACCESS_KEY=your-secret-access-key \
HUAWEICLOUD_REGION=cn-south-1 \
lego --email [email protected] --dns huaweicloud --domains my.example.org run
'''

[Configuration]
[Configuration.Credentials]
HUAWEICLOUD_ACCESS_KEY_ID = "Access key ID"
HUAWEICLOUD_SECRET_ACCESS_KEY = "Access Key secret"
HUAWEICLOUD_REGION = "Region"

[Configuration.Additional]
HUAWEICLOUD_POLLING_INTERVAL = "Time between DNS propagation check"
HUAWEICLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
HUAWEICLOUD_TTL = "The TTL of the TXT record used for the DNS challenge"
HUAWEICLOUD_HTTP_TIMEOUT = "API request timeout"

[Links]
API = "https://console-intl.huaweicloud.com/apiexplorer/#/openapi/DNS/doc?locale=en-us"
CN_API = "https://support.huaweicloud.com/api-dns/zh-cn_topic_0132421999.html"
GoClient = "https://github.com/huaweicloud/huaweicloud-sdk-go-v3"
Loading

0 comments on commit f77681b

Please sign in to comment.