From 99961aab460aa1ce7e55776699001f78c0fe9ef8 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 15 Nov 2025 20:23:05 +0800 Subject: [PATCH] feat(provider): new acme dns-01 provider: qingcloud --- .../certapply/applicators/sp_qingcloud_dns.go | 30 ++++ .../certapply/applicators/sp_ucloud_udnr.go | 6 +- internal/domain/access.go | 5 + internal/domain/provider.go | 5 +- .../providers/35cn/internal/lego.go | 4 +- .../providers/cmcccloud/internal/lego.go | 2 +- .../providers/ctcccloud/internal/lego.go | 2 +- .../providers/dnsla/internal/lego.go | 2 +- .../providers/dynv6/internal/lego.go | 2 +- .../providers/gname/internal/lego.go | 2 +- .../providers/jdcloud/internal/lego.go | 2 +- .../providers/qingcloud/internal/lego.go | 152 ++++++++++++++++++ .../providers/qingcloud/qingcloud.go | 39 +++++ .../{ucloud-udnr => ucloud}/internal/lego.go | 16 +- .../ucloud_udnr.go => ucloud/ucloud.go} | 4 +- .../providers/xinnet/internal/lego.go | 2 +- pkg/sdk3rd/qingcloud/dns/api_create_record.go | 69 ++++++++ pkg/sdk3rd/qingcloud/dns/api_delete_record.go | 40 +++++ pkg/sdk3rd/qingcloud/dns/client.go | 130 +++++++++++++++ pkg/sdk3rd/qingcloud/dns/types.go | 44 +++++ pkg/sdk3rd/xinnet/client.go | 3 +- ui/public/imgs/providers/qingcloud.svg | 1 + .../forms/AccessConfigFieldsProvider.tsx | 2 + .../AccessConfigFieldsProviderQingCloud.tsx | 64 ++++++++ ui/src/domain/provider.ts | 6 + ui/src/i18n/locales/en/nls.access.json | 6 + ui/src/i18n/locales/en/nls.provider.json | 2 + ui/src/i18n/locales/zh/nls.access.json | 6 + ui/src/i18n/locales/zh/nls.provider.json | 2 + 29 files changed, 626 insertions(+), 24 deletions(-) create mode 100644 internal/certapply/applicators/sp_qingcloud_dns.go create mode 100644 pkg/core/ssl-applicator/acme-dns01/providers/qingcloud/internal/lego.go create mode 100644 pkg/core/ssl-applicator/acme-dns01/providers/qingcloud/qingcloud.go rename pkg/core/ssl-applicator/acme-dns01/providers/{ucloud-udnr => ucloud}/internal/lego.go (89%) rename pkg/core/ssl-applicator/acme-dns01/providers/{ucloud-udnr/ucloud_udnr.go => ucloud/ucloud.go} (95%) create mode 100644 pkg/sdk3rd/qingcloud/dns/api_create_record.go create mode 100644 pkg/sdk3rd/qingcloud/dns/api_delete_record.go create mode 100644 pkg/sdk3rd/qingcloud/dns/client.go create mode 100644 pkg/sdk3rd/qingcloud/dns/types.go create mode 100644 ui/public/imgs/providers/qingcloud.svg create mode 100644 ui/src/components/access/forms/AccessConfigFieldsProviderQingCloud.tsx 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",