diff --git a/internal/certapply/applicators/sp_qingcloud_dns.go b/internal/certapply/applicators/sp_qingcloud_dns.go
new file mode 100644
index 000000000..8a677f7c0
--- /dev/null
+++ b/internal/certapply/applicators/sp_qingcloud_dns.go
@@ -0,0 +1,30 @@
+package applicators
+
+import (
+ "fmt"
+
+ "github.com/go-acme/lego/v4/challenge"
+
+ "github.com/certimate-go/certimate/internal/domain"
+ "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/qingcloud"
+ xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
+)
+
+func init() {
+ ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeQingCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {
+ credentials := domain.AccessConfigForQingCloud{}
+ if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
+ return nil, fmt.Errorf("failed to populate provider access config: %w", err)
+ }
+
+ provider, err := qingcloud.NewChallengeProvider(&qingcloud.ChallengeProviderConfig{
+ AccessKeyId: credentials.AccessKeyId,
+ SecretAccessKey: credentials.SecretAccessKey,
+ DnsPropagationTimeout: options.DnsPropagationTimeout,
+ DnsTTL: options.DnsTTL,
+ })
+ return provider, err
+ })
+
+ ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeQingCloud, domain.ACMEDns01ProviderTypeQingCloudDNS)
+}
diff --git a/internal/certapply/applicators/sp_ucloud_udnr.go b/internal/certapply/applicators/sp_ucloud_udnr.go
index 6a4f760cc..944572bdc 100644
--- a/internal/certapply/applicators/sp_ucloud_udnr.go
+++ b/internal/certapply/applicators/sp_ucloud_udnr.go
@@ -6,7 +6,7 @@ import (
"github.com/go-acme/lego/v4/challenge"
"github.com/certimate-go/certimate/internal/domain"
- udnr "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr"
+ "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ucloud"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
@@ -17,7 +17,7 @@ func init() {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
- provider, err := udnr.NewChallengeProvider(&udnr.ChallengeProviderConfig{
+ provider, err := ucloud.NewChallengeProvider(&ucloud.ChallengeProviderConfig{
PrivateKey: credentials.PrivateKey,
PublicKey: credentials.PublicKey,
ProjectId: credentials.ProjectId,
@@ -26,4 +26,6 @@ func init() {
})
return provider, err
})
+
+ ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeUCloud, domain.ACMEDns01ProviderTypeUCloudUDNR)
}
diff --git a/internal/domain/access.go b/internal/domain/access.go
index 8dd5f03e1..97fcc142f 100644
--- a/internal/domain/access.go
+++ b/internal/domain/access.go
@@ -396,6 +396,11 @@ type AccessConfigForProxmoxVE struct {
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
+type AccessConfigForQingCloud struct {
+ AccessKeyId string `json:"accessKeyId"`
+ SecretAccessKey string `json:"secretAccessKey"`
+}
+
type AccessConfigForQiniu struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
diff --git a/internal/domain/provider.go b/internal/domain/provider.go
index 26cdbdabf..13e999080 100644
--- a/internal/domain/provider.go
+++ b/internal/domain/provider.go
@@ -85,7 +85,7 @@ const (
AccessProviderTypePowerDNS = AccessProviderType("powerdns")
AccessProviderTypeProxmoxVE = AccessProviderType("proxmoxve")
AccessProviderTypeQiniu = AccessProviderType("qiniu")
- AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留)
+ AccessProviderTypeQingCloud = AccessProviderType("qingcloud")
AccessProviderTypeRainYun = AccessProviderType("rainyun")
AccessProviderTypeRatPanel = AccessProviderType("ratpanel")
AccessProviderTypeRFC2136 = AccessProviderType("rfc2136")
@@ -197,6 +197,8 @@ const (
ACMEDns01ProviderTypeOVHcloud = ACMEDns01ProviderType(AccessProviderTypeOVHcloud)
ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
+ ACMEDns01ProviderTypeQingCloud = ACMEDns01ProviderType(AccessProviderTypeQingCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeQingCloudDNS]
+ ACMEDns01ProviderTypeQingCloudDNS = ACMEDns01ProviderType(AccessProviderTypeQingCloud + "-dns")
ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun)
ACMEDns01ProviderTypeRFC2136 = ACMEDns01ProviderType(AccessProviderTypeRFC2136)
ACMEDns01ProviderTypeSpaceship = ACMEDns01ProviderType(AccessProviderTypeSpaceship)
@@ -204,6 +206,7 @@ const (
ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS]
ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns")
ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo")
+ ACMEDns01ProviderTypeUCloud = ACMEDns01ProviderType(AccessProviderTypeUCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeUCloudUDNR]
ACMEDns01ProviderTypeUCloudUDNR = ACMEDns01ProviderType(AccessProviderTypeUCloud + "-udnr")
ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel)
ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS]
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/35cn/internal/lego.go b/pkg/core/ssl-applicator/acme-dns01/providers/35cn/internal/lego.go
index c4f8a8071..7f045558b 100644
--- a/pkg/core/ssl-applicator/acme-dns01/providers/35cn/internal/lego.go
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/35cn/internal/lego.go
@@ -40,8 +40,8 @@ type Config struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 60),
- PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
- PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
+ PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
},
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/cmcccloud/internal/lego.go b/pkg/core/ssl-applicator/acme-dns01/providers/cmcccloud/internal/lego.go
index 15162a44e..a29288023 100644
--- a/pkg/core/ssl-applicator/acme-dns01/providers/cmcccloud/internal/lego.go
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/cmcccloud/internal/lego.go
@@ -54,7 +54,7 @@ func NewDefaultConfig() *Config {
ReadTimeout: env.GetOrDefaultInt(EnvReadTimeout, 30),
ConnectTimeout: env.GetOrDefaultInt(EnvConnectTimeout, 30),
TTL: env.GetOrDefaultInt(EnvTTL, 600),
- PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
}
}
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/ctcccloud/internal/lego.go b/pkg/core/ssl-applicator/acme-dns01/providers/ctcccloud/internal/lego.go
index 82bf433b9..f225dae0a 100644
--- a/pkg/core/ssl-applicator/acme-dns01/providers/ctcccloud/internal/lego.go
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/ctcccloud/internal/lego.go
@@ -49,7 +49,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 600),
- PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/dnsla/internal/lego.go b/pkg/core/ssl-applicator/acme-dns01/providers/dnsla/internal/lego.go
index 52f3441ec..a858f3aea 100644
--- a/pkg/core/ssl-applicator/acme-dns01/providers/dnsla/internal/lego.go
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/dnsla/internal/lego.go
@@ -50,7 +50,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 300),
- PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/dynv6/internal/lego.go b/pkg/core/ssl-applicator/acme-dns01/providers/dynv6/internal/lego.go
index f46d39cd9..dafed22ce 100644
--- a/pkg/core/ssl-applicator/acme-dns01/providers/dynv6/internal/lego.go
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/dynv6/internal/lego.go
@@ -41,7 +41,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
- PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
}
}
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/gname/internal/lego.go b/pkg/core/ssl-applicator/acme-dns01/providers/gname/internal/lego.go
index a3cf2c90b..68cb37291 100644
--- a/pkg/core/ssl-applicator/acme-dns01/providers/gname/internal/lego.go
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/gname/internal/lego.go
@@ -49,7 +49,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 300),
- PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/jdcloud/internal/lego.go b/pkg/core/ssl-applicator/acme-dns01/providers/jdcloud/internal/lego.go
index bbef553aa..7199377b3 100644
--- a/pkg/core/ssl-applicator/acme-dns01/providers/jdcloud/internal/lego.go
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/jdcloud/internal/lego.go
@@ -51,7 +51,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 300),
- PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/qingcloud/internal/lego.go b/pkg/core/ssl-applicator/acme-dns01/providers/qingcloud/internal/lego.go
new file mode 100644
index 000000000..97333c385
--- /dev/null
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/qingcloud/internal/lego.go
@@ -0,0 +1,152 @@
+package internal
+
+import (
+ "errors"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/go-acme/lego/v4/challenge"
+ "github.com/go-acme/lego/v4/challenge/dns01"
+ "github.com/go-acme/lego/v4/platform/config/env"
+ "github.com/samber/lo"
+
+ qingcloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/qingcloud/dns"
+)
+
+const (
+ envNamespace = "DNSLA_"
+
+ EnvAccessKey = envNamespace + "ACCESS_KEY"
+ EnvAccessSecret = envNamespace + "ACCESS_SECRET"
+
+ EnvTTL = envNamespace + "TTL"
+ EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
+ EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
+ EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
+)
+
+var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
+
+type Config struct {
+ AccessKey string
+ AccessSecret string
+
+ PropagationTimeout time.Duration
+ PollingInterval time.Duration
+ TTL int
+ HTTPTimeout time.Duration
+}
+
+type DNSProvider struct {
+ client *qingcloudsdk.Client
+ config *Config
+
+ recordIDs map[string]*int64
+ recordIDsMu sync.Mutex
+}
+
+func NewDefaultConfig() *Config {
+ return &Config{
+ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
+ PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
+ HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
+ }
+}
+
+func NewDNSProvider() (*DNSProvider, error) {
+ values, err := env.Get(EnvAccessKey, EnvAccessSecret)
+ if err != nil {
+ return nil, fmt.Errorf("qingcloud: %w", err)
+ }
+
+ config := NewDefaultConfig()
+ config.AccessKey = values[EnvAccessKey]
+ config.AccessSecret = values[EnvAccessSecret]
+
+ return NewDNSProviderConfig(config)
+}
+
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+ if config == nil {
+ return nil, errors.New("qingcloud: the configuration of the DNS provider is nil")
+ }
+
+ client, err := qingcloudsdk.NewClient(config.AccessKey, config.AccessSecret)
+ if err != nil {
+ return nil, err
+ } else {
+ client.SetTimeout(config.HTTPTimeout)
+ }
+
+ return &DNSProvider{
+ client: client,
+ config: config,
+ recordIDs: make(map[string]*int64),
+ recordIDsMu: sync.Mutex{},
+ }, nil
+}
+
+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("qingcloud: could not find zone for domain %q: %w", domain, err)
+ }
+
+ // REF: https://docsv4.qingcloud.com/user_guide/development_docs/api/api_list/dns/record/#_createrecord
+ qingcloudCreateRecordReq := &qingcloudsdk.CreateRecordRequest{
+ ZoneName: lo.ToPtr(authZone),
+ DomainName: lo.ToPtr(info.EffectiveFQDN),
+ ViewId: lo.ToPtr(int32(0)),
+ Type: lo.ToPtr("TXT"),
+ Ttl: lo.ToPtr(int32(d.config.TTL)),
+ Records: []*qingcloudsdk.CreateRecordRequestRecord{
+ {
+ Values: []*qingcloudsdk.CreateRecordRequestRecordValue{
+ {
+ Value: lo.ToPtr(info.Value),
+ Status: lo.ToPtr(int32(1)),
+ },
+ },
+ Weight: lo.ToPtr(int32(0)),
+ },
+ },
+ Mode: lo.ToPtr(int32(1)),
+ AutoMerge: lo.ToPtr(int32(1)),
+ }
+ qingcloudCreateRecordResp, err := d.client.CreateRecord(qingcloudCreateRecordReq)
+ if err != nil {
+ return fmt.Errorf("qingcloud: error when create record: %w", err)
+ }
+
+ d.recordIDsMu.Lock()
+ d.recordIDs[token] = qingcloudCreateRecordResp.DomainRecordId
+ d.recordIDsMu.Unlock()
+
+ return nil
+}
+
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+
+ d.recordIDsMu.Lock()
+ recordID, ok := d.recordIDs[token]
+ d.recordIDsMu.Unlock()
+ if !ok {
+ return fmt.Errorf("qingcloud: unknown record ID for '%s'", info.EffectiveFQDN)
+ }
+
+ // REF: https://docsv4.qingcloud.com/user_guide/development_docs/api/api_list/dns/record/#_deleterecord
+ if _, err := d.client.DeleteRecord([]*int64{recordID}); err != nil {
+ return fmt.Errorf("qingcloud: error when delete record: %w", err)
+ }
+
+ return nil
+}
+
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return d.config.PropagationTimeout, d.config.PollingInterval
+}
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/qingcloud/qingcloud.go b/pkg/core/ssl-applicator/acme-dns01/providers/qingcloud/qingcloud.go
new file mode 100644
index 000000000..18854c456
--- /dev/null
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/qingcloud/qingcloud.go
@@ -0,0 +1,39 @@
+package qingcloud
+
+import (
+ "errors"
+ "time"
+
+ "github.com/certimate-go/certimate/pkg/core"
+ "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/qingcloud/internal"
+)
+
+type ChallengeProviderConfig struct {
+ AccessKeyId string `json:"accessKeyId"`
+ SecretAccessKey string `json:"apiPassword"`
+ DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"`
+ DnsTTL int `json:"dnsTTL,omitempty"`
+}
+
+func NewChallengeProvider(config *ChallengeProviderConfig) (core.ACMEChallenger, error) {
+ if config == nil {
+ return nil, errors.New("the configuration of the acme challenge provider is nil")
+ }
+
+ providerConfig := internal.NewDefaultConfig()
+ providerConfig.AccessKey = config.AccessKeyId
+ providerConfig.AccessSecret = config.SecretAccessKey
+ if config.DnsPropagationTimeout != 0 {
+ providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
+ }
+ if config.DnsTTL != 0 {
+ providerConfig.TTL = config.DnsTTL
+ }
+
+ provider, err := internal.NewDNSProviderConfig(providerConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return provider, nil
+}
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr/internal/lego.go b/pkg/core/ssl-applicator/acme-dns01/providers/ucloud/internal/lego.go
similarity index 89%
rename from pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr/internal/lego.go
rename to pkg/core/ssl-applicator/acme-dns01/providers/ucloud/internal/lego.go
index 644f9e64e..25ca91809 100644
--- a/pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr/internal/lego.go
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/ucloud/internal/lego.go
@@ -48,7 +48,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 600),
- PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
@@ -57,7 +57,7 @@ func NewDefaultConfig() *Config {
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvPrivateKey, EnvPublicKey, EnvProjectId)
if err != nil {
- return nil, fmt.Errorf("ucloud-udnr: %w", err)
+ return nil, fmt.Errorf("ucloud: %w", err)
}
config := NewDefaultConfig()
@@ -70,7 +70,7 @@ func NewDNSProvider() (*DNSProvider, error) {
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
- return nil, errors.New("ucloud-udnr: the configuration of the DNS provider is nil")
+ return nil, errors.New("ucloud: the configuration of the DNS provider is nil")
}
cfg := ucloud.NewConfig()
@@ -91,7 +91,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
- return fmt.Errorf("ucloud-udnr: could not find zone for domain %q: %w", domain, err)
+ return fmt.Errorf("ucloud: could not find zone for domain %q: %w", domain, err)
}
// REF: https://docs.ucloud.cn/api/udnr-api/udnr_domain_dns_add
@@ -105,7 +105,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
udnrDomainDNSAddReq.SetProjectId(d.config.ProjectId)
}
if _, err := d.client.AddDomainDNS(udnrDomainDNSAddReq); err != nil {
- return fmt.Errorf("ucloud-udnr: error when create record: %w", err)
+ return fmt.Errorf("ucloud: error when create record: %w", err)
}
return nil
@@ -116,7 +116,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
- return fmt.Errorf("ucloud-udnr: could not find zone for domain %q: %w", domain, err)
+ return fmt.Errorf("ucloud: could not find zone for domain %q: %w", domain, err)
}
// REF: https://docs.ucloud.cn/api/udnr-api/udnr_domain_dns_query
@@ -127,7 +127,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
udnrDomainDNSQueryResp, err := d.client.QueryDomainDNS(udnrDomainDNSQueryReq)
if err != nil {
- return fmt.Errorf("ucloud-udnr: error when list records: %w", err)
+ return fmt.Errorf("ucloud: error when list records: %w", err)
}
// REF: https://docs.ucloud.cn/api/udnr-api/udnr_delete_dns_record
@@ -143,7 +143,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
_, err := d.client.DeleteDomainDNS(udnrDomainDNSDeleteReq)
if err != nil {
- return fmt.Errorf("ucloud-udnr: error when delete record: %w", err)
+ return fmt.Errorf("ucloud: error when delete record: %w", err)
}
break
}
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr/ucloud_udnr.go b/pkg/core/ssl-applicator/acme-dns01/providers/ucloud/ucloud.go
similarity index 95%
rename from pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr/ucloud_udnr.go
rename to pkg/core/ssl-applicator/acme-dns01/providers/ucloud/ucloud.go
index 8c36f33fa..7b4750e73 100644
--- a/pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr/ucloud_udnr.go
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/ucloud/ucloud.go
@@ -1,11 +1,11 @@
-package ucloududnr
+package ucloud
import (
"errors"
"time"
"github.com/certimate-go/certimate/pkg/core"
- "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ucloud-udnr/internal"
+ "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ucloud/internal"
)
type ChallengeProviderConfig struct {
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/internal/lego.go b/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/internal/lego.go
index 6a4a0d3a1..6d62accf8 100644
--- a/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/internal/lego.go
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/xinnet/internal/lego.go
@@ -49,7 +49,7 @@ type DNSProvider struct {
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 600),
- PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
diff --git a/pkg/sdk3rd/qingcloud/dns/api_create_record.go b/pkg/sdk3rd/qingcloud/dns/api_create_record.go
new file mode 100644
index 000000000..59a4218d8
--- /dev/null
+++ b/pkg/sdk3rd/qingcloud/dns/api_create_record.go
@@ -0,0 +1,69 @@
+package dns
+
+import (
+ "context"
+ "net/http"
+)
+
+type CreateRecordRequest struct {
+ ZoneName *string `json:"zone_name,omitempty"`
+ DomainName *string `json:"domain_name,omitempty"`
+ ViewId *int32 `json:"view_id,omitempty"`
+ Type *string `json:"type,omitempty"`
+ Records []*CreateRecordRequestRecord `json:"record,omitempty"`
+ Ttl *int32 `json:"ttl,omitempty"`
+ Mode *int32 `json:"mode,omitempty"`
+ AutoMerge *int32 `json:"auto_merge,omitempty"`
+}
+
+type CreateRecordRequestRecord struct {
+ Values []*CreateRecordRequestRecordValue `json:"values,omitempty"`
+ Weight *int32 `json:"weight,omitempty"`
+}
+
+type CreateRecordRequestRecordValue struct {
+ Value *string `json:"value,omitempty"`
+ Status *int32 `json:"status,omitempty"`
+}
+
+type CreateRecordResponse struct {
+ apiResponseBase
+ DomainName *string `json:"domain_name,omitempty"`
+ DomainRecordId *int64 `json:"domain_record_id,omitempty"`
+ ViewId *int64 `json:"view_id,omitempty"`
+ Records []*CreateRecordResponseRecord `json:"records,omitempty"`
+}
+
+type CreateRecordResponseRecord struct {
+ GroupId *int64 `json:"group_id,omitempty"`
+ GroupStatus *int32 `json:"group_status,omitempty"`
+ Values []*CreateRecordResponseRecordValue `json:"value,omitempty"`
+ Weight *int32 `json:"weight,omitempty"`
+}
+
+type CreateRecordResponseRecordValue struct {
+ ValueId *int64 `json:"id,omitempty"`
+ Value *string `json:"value,omitempty"`
+ Status *int32 `json:"status,omitempty"`
+}
+
+func (c *Client) CreateRecord(req *CreateRecordRequest) (*CreateRecordResponse, error) {
+ return c.CreateRecordWithContext(context.Background(), req)
+}
+
+func (c *Client) CreateRecordWithContext(ctx context.Context, req *CreateRecordRequest) (*CreateRecordResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v1/record/")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &CreateRecordResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/pkg/sdk3rd/qingcloud/dns/api_delete_record.go b/pkg/sdk3rd/qingcloud/dns/api_delete_record.go
new file mode 100644
index 000000000..7a297ae26
--- /dev/null
+++ b/pkg/sdk3rd/qingcloud/dns/api_delete_record.go
@@ -0,0 +1,40 @@
+package dns
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+)
+
+type DeleteRecordResponse struct {
+ apiResponseBase
+}
+
+func (c *Client) DeleteRecord(recordIds []*int64) (*DeleteRecordResponse, error) {
+ return c.DeleteRecordWithContext(context.Background(), recordIds)
+}
+
+func (c *Client) DeleteRecordWithContext(ctx context.Context, recordIds []*int64) (*DeleteRecordResponse, error) {
+ if len(recordIds) == 0 {
+ return nil, fmt.Errorf("sdkerr: unset recordIds")
+ }
+
+ httpreq, err := c.newRequest(http.MethodPost, "/v1/change_record_status/")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(map[string]any{
+ "ids": recordIds,
+ "action": "delete",
+ "target": "record",
+ })
+ httpreq.SetContext(ctx)
+ }
+
+ result := &DeleteRecordResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/pkg/sdk3rd/qingcloud/dns/client.go b/pkg/sdk3rd/qingcloud/dns/client.go
new file mode 100644
index 000000000..3c8a71847
--- /dev/null
+++ b/pkg/sdk3rd/qingcloud/dns/client.go
@@ -0,0 +1,130 @@
+package dns
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+)
+
+type Client struct {
+ client *resty.Client
+}
+
+func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
+ if accessKeyId == "" {
+ return nil, fmt.Errorf("sdkerr: unset accessKeyId")
+ }
+ if secretAccessKey == "" {
+ return nil, fmt.Errorf("sdkerr: unset secretAccessKey")
+ }
+
+ client := resty.New().
+ SetBaseURL("http://api.routewize.com").
+ SetHeader("Accept", "application/json").
+ SetHeader("Content-Type", "application/json").
+ SetHeader("Host", "api.routewize.com").
+ SetHeader("User-Agent", "certimate").
+ SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
+ // 生成时间
+ date := time.Now().UTC().Format(time.RFC1123)
+
+ // 获取请求谓词
+ verb := req.Method
+
+ // 获取访问资源
+ canonicalizedResource := "/"
+ if req.URL != nil {
+ canonicalizedResource = req.URL.Path
+ if req.URL.RawQuery != "" {
+ values, _ := url.ParseQuery(req.URL.RawQuery)
+ canonicalizedResource += "?" + values.Encode()
+ }
+ }
+
+ // 计算签名
+ stringToSign := verb + "\n" +
+ date + "\n" +
+ canonicalizedResource
+ h := hmac.New(sha256.New, []byte(secretAccessKey))
+ h.Write([]byte(stringToSign))
+ sign := base64.StdEncoding.EncodeToString(h.Sum(nil))
+
+ // 设置请求头
+ req.Header.Set("Date", date)
+ req.Header.Set("Authorization", fmt.Sprintf("QC-HMAC-SHA256 %s:%s", accessKeyId, sign))
+
+ return nil
+ })
+
+ return &Client{client}, nil
+}
+
+func (c *Client) SetTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
+ if method == "" {
+ return nil, fmt.Errorf("sdkerr: unset method")
+ }
+ if path == "" {
+ return nil, fmt.Errorf("sdkerr: unset path")
+ }
+
+ req := c.client.R()
+ req.Method = method
+ req.URL = path
+ return req, nil
+}
+
+func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
+ if req == nil {
+ return nil, fmt.Errorf("sdkerr: nil request")
+ }
+
+ // WARN:
+ // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.
+
+ resp, err := req.Send()
+ if err != nil {
+ return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
+ } else if resp.IsError() {
+ return resp, fmt.Errorf("sdkerr: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
+ }
+
+ return resp, nil
+}
+
+func (c *Client) doRequestWithResult(req *resty.Request, res apiResponse) (*resty.Response, error) {
+ if req == nil {
+ return nil, fmt.Errorf("sdkerr: nil request")
+ }
+
+ resp, err := c.doRequest(req)
+ if err != nil {
+ if resp != nil {
+ json.Unmarshal(resp.Body(), &res)
+ }
+ return resp, err
+ }
+
+ if len(resp.Body()) != 0 {
+ if err := json.Unmarshal(resp.Body(), &res); err != nil {
+ return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w", err)
+ } else {
+ if tcode := res.GetCode(); tcode != 0 {
+ return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage())
+ }
+ }
+ }
+
+ return resp, nil
+}
diff --git a/pkg/sdk3rd/qingcloud/dns/types.go b/pkg/sdk3rd/qingcloud/dns/types.go
new file mode 100644
index 000000000..7123d3c2f
--- /dev/null
+++ b/pkg/sdk3rd/qingcloud/dns/types.go
@@ -0,0 +1,44 @@
+package dns
+
+type apiResponse interface {
+ GetCode() int
+ GetMessage() string
+}
+
+type apiResponseBase struct {
+ Code *int `json:"code,omitempty"`
+ Message *string `json:"message,omitempty"`
+}
+
+func (r *apiResponseBase) GetCode() int {
+ if r.Code == nil {
+ return 0
+ }
+
+ return *r.Code
+}
+
+func (r *apiResponseBase) GetMessage() string {
+ if r.Message == nil {
+ return ""
+ }
+
+ return *r.Message
+}
+
+var _ apiResponse = (*apiResponseBase)(nil)
+
+type DnsRecord struct {
+ GroupId *int64 `json:"group_id,omitempty"`
+ GroupStatus *int32 `json:"group_status,omitempty"`
+ Value []*DnsRecordValue `json:"value,omitempty"`
+ Weight *int32 `json:"weight,omitempty"`
+}
+
+type DnsRecordValue struct {
+ Id *int64 `json:"id,omitempty"`
+ Type *string `json:"type,omitempty"`
+ Value *string `json:"value,omitempty"`
+ Line *string `json:"line,omitempty"`
+ Ttl *int32 `json:"ttl,omitempty"`
+}
diff --git a/pkg/sdk3rd/xinnet/client.go b/pkg/sdk3rd/xinnet/client.go
index 0317545c8..092897f56 100644
--- a/pkg/sdk3rd/xinnet/client.go
+++ b/pkg/sdk3rd/xinnet/client.go
@@ -73,8 +73,7 @@ func NewClient(agentId, appSecret string) (*Client, error) {
requestMethod + "\n" +
urlPath + "\n" +
requestBody
- key := []byte(appSecret)
- h := hmac.New(sha256.New, key)
+ h := hmac.New(sha256.New, []byte(appSecret))
h.Write([]byte(stringToSign))
signature := hex.EncodeToString(h.Sum(nil))
diff --git a/ui/public/imgs/providers/qingcloud.svg b/ui/public/imgs/providers/qingcloud.svg
new file mode 100644
index 000000000..923ed910e
--- /dev/null
+++ b/ui/public/imgs/providers/qingcloud.svg
@@ -0,0 +1 @@
+
diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx
index e6b8baaea..0e605a624 100644
--- a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx
+++ b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx
@@ -72,6 +72,7 @@ import AccessConfigFieldsProviderOVHcloud from "./AccessConfigFieldsProviderOVHc
import AccessConfigFieldsProviderPorkbun from "./AccessConfigFieldsProviderPorkbun";
import AccessConfigFieldsProviderPowerDNS from "./AccessConfigFieldsProviderPowerDNS";
import AccessConfigFieldsProviderProxmoxVE from "./AccessConfigFieldsProviderProxmoxVE";
+import AccessConfigFieldsProviderQingCloud from "./AccessConfigFieldsProviderQingCloud";
import AccessConfigFieldsProviderQiniu from "./AccessConfigFieldsProviderQiniu";
import AccessConfigFieldsProviderRainYun from "./AccessConfigFieldsProviderRainYun";
import AccessConfigFieldsProviderRatPanel from "./AccessConfigFieldsProviderRatPanel";
@@ -173,6 +174,7 @@ const providerComponentMap: Partial {
+ const { i18n, t } = useTranslation();
+
+ const { parentNamePath } = useFormNestedFieldsContext();
+ const formSchema = z.object({
+ [parentNamePath]: getSchema({ i18n }),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+ const initialValues = getInitialValues();
+
+ return (
+ <>
+ }
+ >
+
+
+
+ }
+ >
+
+
+ >
+ );
+};
+
+const getInitialValues = (): Nullish>> => {
+ return {
+ accessKeyId: "",
+ secretAccessKey: "",
+ };
+};
+
+const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
+ const { t } = i18n;
+
+ return z.object({
+ accessKeyId: z.string().nonempty(t("access.form.qingcloud_access_key_id.placeholder")),
+ secretAccessKey: z.string().nonempty(t("access.form.qingcloud_secret_access_key.placeholder")),
+ });
+};
+
+const _default = Object.assign(AccessConfigFormFieldsProviderQingCloud, {
+ getInitialValues,
+ getSchema,
+});
+
+export default _default;
diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts
index 31b4c6b70..d22d85e61 100644
--- a/ui/src/domain/provider.ts
+++ b/ui/src/domain/provider.ts
@@ -88,6 +88,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
PORKBUN: "porkbun",
POWERDNS: "powerdns",
PROXMOXVE: "proxmoxve",
+ QINGCLOUD: "qingcloud",
QINIU: "qiniu",
RAINYUN: "rainyun",
RATPANEL: "ratpanel",
@@ -213,6 +214,7 @@ export const accessProvidersMap: Maphttps://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens",
+ "access.form.qingcloud_access_key_id.label": "QingCloud AccessKeyID",
+ "access.form.qingcloud_access_key_id.placeholder": "Please enter QingCloud AccessKeyID",
+ "access.form.qingcloud_access_key_id.tooltip": "For more information, see https://console.qingcloud.com/access_keys/",
+ "access.form.qingcloud_secret_access_key.label": "QingCloud SecretAccessKey",
+ "access.form.qingcloud_secret_access_key.placeholder": "Please enter QingCloud SecretAccessKey",
+ "access.form.qingcloud_secret_access_key.tooltip": "For more information, see https://console.qingcloud.com/access_keys/",
"access.form.qiniu_access_key.label": "Qiniu AccessKey",
"access.form.qiniu_access_key.placeholder": "Please enter Qiniu AccessKey",
"access.form.qiniu_access_key.tooltip": "For more information, see https://portal.qiniu.com/",
diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json
index 30bd7c9e8..5a51820b7 100644
--- a/ui/src/i18n/locales/en/nls.provider.json
+++ b/ui/src/i18n/locales/en/nls.provider.json
@@ -139,6 +139,8 @@
"provider.porkbun": "Porkbun",
"provider.powerdns": "PowerDNS",
"provider.proxmoxve": "Proxmox VE",
+ "provider.qingcloud": "QingCloud",
+ "provider.qingcloud.dns": "QingCloud - DNS",
"provider.qiniu": "Qiniu",
"provider.qiniu.cdn": "Qiniu - CDN (Content Delivery Network)",
"provider.qiniu.kodo": "Qiniu - Kodo",
diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json
index 9475c8a8c..f9766aaf1 100644
--- a/ui/src/i18n/locales/zh/nls.access.json
+++ b/ui/src/i18n/locales/zh/nls.access.json
@@ -470,6 +470,12 @@
"access.form.proxmoxve_api_token_secret.label": "Proxmox VE API Token Secret(可选)",
"access.form.proxmoxve_api_token_secret.placeholder": "请输入 Proxmox VE API Token Secret",
"access.form.proxmoxve_api_token_secret.tooltip": "这是什么?请参阅 https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens",
+ "access.form.qingcloud_access_key_id.label": "青云 AccessKeyID",
+ "access.form.qingcloud_access_key_id.placeholder": "请输入青云 AccessKeyID",
+ "access.form.qingcloud_access_key_id.tooltip": "这是什么?请参阅 https://console.qingcloud.com/access_keys/",
+ "access.form.qingcloud_secret_access_key.label": "青云 SecretAccessKey",
+ "access.form.qingcloud_secret_access_key.placeholder": "请输入青云 SecretAccessKey",
+ "access.form.qingcloud_secret_access_key.tooltip": "这是什么?请参阅 https://console.qingcloud.com/access_keys/",
"access.form.qiniu_access_key.label": "七牛云 AccessKey",
"access.form.qiniu_access_key.placeholder": "请输入七牛云 AccessKey",
"access.form.qiniu_access_key.tooltip": "这是什么?请参阅 https://portal.qiniu.com/",
diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json
index 17490ae2f..619f13589 100644
--- a/ui/src/i18n/locales/zh/nls.provider.json
+++ b/ui/src/i18n/locales/zh/nls.provider.json
@@ -139,6 +139,8 @@
"provider.porkbun": "Porkbun",
"provider.powerdns": "PowerDNS",
"provider.proxmoxve": "Proxmox VE",
+ "provider.qingcloud": "青云",
+ "provider.qingcloud.dns": "青云 - 云解析 DNS",
"provider.qiniu": "七牛云",
"provider.qiniu.cdn": "七牛云 - 内容分发网络 CDN",
"provider.qiniu.kodo": "七牛云 - 对象存储 Kodo",