From 9c8ab98efb31b488b08a8258ba104e66449a38d3 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 12 Jun 2025 23:05:18 +0800 Subject: [PATCH] feat: new acme dns-01 provider: statecloud smartdns --- internal/applicant/providers.go | 19 +- internal/domain/access.go | 5 + internal/domain/provider.go | 95 ++++---- .../lego-providers/cmcccloud/cmcccloud.go | 3 +- .../lego-providers/cmcccloud/internal/lego.go | 14 +- .../lego-providers/ctcccloud/ctcccloud.go | 39 ++++ .../lego-providers/ctcccloud/internal/lego.go | 203 ++++++++++++++++++ .../pkg/sdk3rd/ctyun/dns/api_add_record.go | 46 ++++ .../pkg/sdk3rd/ctyun/dns/api_delete_record.go | 35 +++ .../sdk3rd/ctyun/dns/api_query_record_list.go | 53 +++++ .../pkg/sdk3rd/ctyun/dns/api_update_record.go | 47 ++++ internal/pkg/sdk3rd/ctyun/dns/client.go | 40 ++++ internal/pkg/sdk3rd/ctyun/dns/types.go | 11 + internal/pkg/sdk3rd/ctyun/openapi/client.go | 167 ++++++++++++++ ui/public/imgs/providers/ctcccloud.svg | 1 + ui/src/components/access/AccessForm.tsx | 3 + .../access/AccessFormCTCCCloudConfig.tsx | 73 +++++++ ui/src/domain/access.ts | 6 + ui/src/domain/provider.ts | 10 +- ui/src/i18n/locales/en/nls.access.json | 6 + ui/src/i18n/locales/en/nls.provider.json | 14 +- ui/src/i18n/locales/zh/nls.access.json | 6 + ui/src/i18n/locales/zh/nls.provider.json | 6 +- 23 files changed, 837 insertions(+), 65 deletions(-) create mode 100644 internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/ctcccloud.go create mode 100644 internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/internal/lego.go create mode 100644 internal/pkg/sdk3rd/ctyun/dns/api_add_record.go create mode 100644 internal/pkg/sdk3rd/ctyun/dns/api_delete_record.go create mode 100644 internal/pkg/sdk3rd/ctyun/dns/api_query_record_list.go create mode 100644 internal/pkg/sdk3rd/ctyun/dns/api_update_record.go create mode 100644 internal/pkg/sdk3rd/ctyun/dns/client.go create mode 100644 internal/pkg/sdk3rd/ctyun/dns/types.go create mode 100644 internal/pkg/sdk3rd/ctyun/openapi/client.go create mode 100644 ui/public/imgs/providers/ctcccloud.svg create mode 100644 ui/src/components/access/AccessFormCTCCCloudConfig.tsx diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index fbf24742..3dbfd79e 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -17,6 +17,7 @@ import ( pClouDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cloudns" pCMCCCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud" pConstellix "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/constellix" + pCTCCCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud" pDeSEC "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/desec" pDigitalOcean "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean" pDNSLA "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dnsla" @@ -220,7 +221,7 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi return applicant, err } - case domain.ACMEDns01ProviderTypeCMCCCloud: + case domain.ACMEDns01ProviderTypeCMCCCloud, domain.ACMEDns01ProviderTypeCMCCCloudDNS: { access := domain.AccessConfigForCMCCCloud{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -252,6 +253,22 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi return applicant, err } + case domain.ACMEDns01ProviderTypeCTCCCloud, domain.ACMEDns01ProviderTypeCTCCCloudSmartDNS: + { + access := domain.AccessConfigForCTCCCloud{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + applicant, err := pCTCCCloud.NewChallengeProvider(&pCTCCCloud.ChallengeProviderConfig{ + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + } + case domain.ACMEDns01ProviderTypeDeSEC: { access := domain.AccessConfigForDeSEC{} diff --git a/internal/domain/access.go b/internal/domain/access.go index 4cce40a2..29d07513 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -120,6 +120,11 @@ type AccessConfigForConstellix struct { SecretKey string `json:"secretKey"` } +type AccessConfigForCTCCCloud struct { + AccessKeyId string `json:"accessKeyId"` + SecretAccessKey string `json:"secretAccessKey"` +} + type AccessConfigForDeSEC struct { Token string `json:"token"` } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 2ca69241..b10cda8f 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -30,7 +30,7 @@ const ( AccessProviderTypeClouDNS = AccessProviderType("cloudns") AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud") AccessProviderTypeConstellix = AccessProviderType("constellix") - AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 天翼云(预留) + AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 联通云(预留) AccessProviderTypeDeSEC = AccessProviderType("desec") AccessProviderTypeDigitalOcean = AccessProviderType("digitalocean") @@ -119,51 +119,54 @@ ACME DNS-01 提供商常量值。 NOTICE: If you add new constant, please keep ASCII order. */ const ( - ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq) - ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS] - ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns") - ACMEDns01ProviderTypeAliyunESA = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-esa") - ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53] - ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53") - ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure] - ACMEDns01ProviderTypeAzureDNS = ACMEDns01ProviderType(AccessProviderTypeAzure + "-dns") - ACMEDns01ProviderTypeBaiduCloud = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeBaiduCloudDNS] - ACMEDns01ProviderTypeBaiduCloudDNS = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud + "-dns") - ACMEDns01ProviderTypeBunny = ACMEDns01ProviderType(AccessProviderTypeBunny) - ACMEDns01ProviderTypeCloudflare = ACMEDns01ProviderType(AccessProviderTypeCloudflare) - ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS) - ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud) - ACMEDns01ProviderTypeConstellix = ACMEDns01ProviderType(AccessProviderTypeConstellix) - ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC) - ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean) - ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA) - ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS) - ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6) - ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore) - ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname) - ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy) - ACMEDns01ProviderTypeHetzner = ACMEDns01ProviderType(AccessProviderTypeHetzner) - ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS] - ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns") - ACMEDns01ProviderTypeJDCloud = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeJDCloudDNS] - ACMEDns01ProviderTypeJDCloudDNS = ACMEDns01ProviderType(AccessProviderTypeJDCloud + "-dns") - ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap) - ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom) - ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo) - ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup) - ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify) - ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1) - ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun) - ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS) - ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun) - ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS] - ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns") - ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo") - ACMEDns01ProviderTypeUCloudUDNR = ACMEDns01ProviderType(AccessProviderTypeUCloud + "-udnr") - ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel) - ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS] - ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns") - ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn) + ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq) + ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS] + ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns") + ACMEDns01ProviderTypeAliyunESA = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-esa") + ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53] + ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53") + ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure] + ACMEDns01ProviderTypeAzureDNS = ACMEDns01ProviderType(AccessProviderTypeAzure + "-dns") + ACMEDns01ProviderTypeBaiduCloud = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeBaiduCloudDNS] + ACMEDns01ProviderTypeBaiduCloudDNS = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud + "-dns") + ACMEDns01ProviderTypeBunny = ACMEDns01ProviderType(AccessProviderTypeBunny) + ACMEDns01ProviderTypeCloudflare = ACMEDns01ProviderType(AccessProviderTypeCloudflare) + ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS) + ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCMCCCloudDNS] + ACMEDns01ProviderTypeCMCCCloudDNS = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud + "-dns") + ACMEDns01ProviderTypeConstellix = ACMEDns01ProviderType(AccessProviderTypeConstellix) + ACMEDns01ProviderTypeCTCCCloud = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCTCCCloudSmartDNS] + ACMEDns01ProviderTypeCTCCCloudSmartDNS = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud + "-smartdns") + ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC) + ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean) + ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA) + ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS) + ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6) + ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore) + ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname) + ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy) + ACMEDns01ProviderTypeHetzner = ACMEDns01ProviderType(AccessProviderTypeHetzner) + ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS] + ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns") + ACMEDns01ProviderTypeJDCloud = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeJDCloudDNS] + ACMEDns01ProviderTypeJDCloudDNS = ACMEDns01ProviderType(AccessProviderTypeJDCloud + "-dns") + ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap) + ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom) + ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo) + ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup) + ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify) + ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1) + ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun) + ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS) + ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun) + ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS] + ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns") + ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo") + ACMEDns01ProviderTypeUCloudUDNR = ACMEDns01ProviderType(AccessProviderTypeUCloud + "-udnr") + ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel) + ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS] + ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns") + ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn) ) type DeploymentProviderType string diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/cmcccloud.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/cmcccloud.go index ba0721fd..83425f2d 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/cmcccloud.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/cmcccloud.go @@ -1,7 +1,6 @@ package cmcccloud import ( - "errors" "time" "github.com/go-acme/lego/v4/challenge" @@ -18,7 +17,7 @@ type ChallengeProviderConfig struct { func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) { if config == nil { - return nil, errors.New("config is nil") + panic("config is nil") } providerConfig := internal.NewDefaultConfig() diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go index 6bccb1dc..b4d6b971 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go @@ -18,8 +18,9 @@ import ( const ( envNamespace = "CMCCCLOUD_" - EnvAccessKey = envNamespace + "ACCESS_KEY" - EnvSecretKey = envNamespace + "SECRET_KEY" + EnvAccessKey = envNamespace + "ACCESS_KEY" + EnvSecretKey = envNamespace + "SECRET_KEY" + EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" @@ -30,13 +31,14 @@ const ( var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { - AccessKey string - SecretKey string - ReadTimeOut int - ConnectTimeout int + AccessKey string + SecretKey string + PropagationTimeout time.Duration PollingInterval time.Duration TTL int32 + ReadTimeOut int + ConnectTimeout int } type DNSProvider struct { diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/ctcccloud.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/ctcccloud.go new file mode 100644 index 00000000..8b3d494a --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/ctcccloud.go @@ -0,0 +1,39 @@ +package ctcccloud + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + + "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/internal" +) + +type ChallengeProviderConfig struct { + AccessKeyId string `json:"accessKeyId"` + SecretAccessKey string `json:"secretAccessKey"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) { + if config == nil { + panic("config is nil") + } + + providerConfig := internal.NewDefaultConfig() + providerConfig.AccessKeyId = config.AccessKeyId + providerConfig.SecretAccessKey = config.SecretAccessKey + if config.DnsTTL != 0 { + providerConfig.TTL = int(config.DnsTTL) + } + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + + provider, err := internal.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/internal/lego.go new file mode 100644 index 00000000..1dd7f2e0 --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/internal/lego.go @@ -0,0 +1,203 @@ +package internal + +import ( + "errors" + "fmt" + "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" + + ctyundns "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/dns" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" +) + +const ( + envNamespace = "CTYUNSMARTDNS_" + + EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID" + EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +type Config struct { + AccessKeyId string + SecretAccessKey string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPTimeout time.Duration +} + +type DNSProvider struct { + client *ctyundns.Client + config *Config +} + +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 600), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + } +} + +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey) + if err != nil { + return nil, fmt.Errorf("ctyun: %w", err) + } + + config := NewDefaultConfig() + config.AccessKeyId = values[EnvAccessKeyID] + config.SecretAccessKey = values[EnvSecretAccessKey] + + return NewDNSProviderConfig(config) +} + +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("ctyun: the configuration of the DNS provider is nil") + } + + client, err := ctyundns.NewClient(config.AccessKeyId, config.SecretAccessKey) + if err != nil { + return nil, err + } else { + client.SetTimeout(config.HTTPTimeout) + } + + return &DNSProvider{ + client: client, + config: config, + }, 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("ctyun: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("ctyun: %w", err) + } + + if err := d.addOrUpdateDNSRecord(dns01.UnFqdn(authZone), subDomain, info.Value); err != nil { + return fmt.Errorf("ctyun: %w", err) + } + + return nil +} + +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("ctyun: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("ctyun: %w", err) + } + + if err := d.removeDNSRecord(dns01.UnFqdn(authZone), subDomain); err != nil { + return fmt.Errorf("ctyun: %w", err) + } + + return nil +} + +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) findDNSRecordId(zoneName, subDomain string) (int32, error) { + // 查询解析记录列表 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11264&data=181&isNormal=1&vid=259 + request := &ctyundns.QueryRecordListRequest{} + request.Domain = typeutil.ToPtr(zoneName) + request.Host = typeutil.ToPtr(subDomain) + request.Type = typeutil.ToPtr("TXT") + + response, err := d.client.QueryRecordList(request) + if err != nil { + return 0, err + } + + if response.ReturnObj == nil || response.ReturnObj.Records == nil || len(response.ReturnObj.Records) == 0 { + return 0, nil + } + + return response.ReturnObj.Records[0].RecordId, nil +} + +func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) error { + recordId, err := d.findDNSRecordId(zoneName, subDomain) + if err != nil { + return err + } + + if recordId == 0 { + // 新增解析记录 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11259&data=181&isNormal=1&vid=259 + request := &ctyundns.AddRecordRequest{ + Domain: typeutil.ToPtr(zoneName), + Host: typeutil.ToPtr(subDomain), + Type: typeutil.ToPtr("TXT"), + LineCode: typeutil.ToPtr("Default"), + Value: typeutil.ToPtr(value), + State: typeutil.ToPtr(int32(1)), + TTL: typeutil.ToPtr(int32(d.config.TTL)), + } + _, err := d.client.AddRecord(request) + return err + } else { + // 修改解析记录 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11261&data=181&isNormal=1&vid=259 + request := &ctyundns.UpdateRecordRequest{ + RecordId: typeutil.ToPtr(recordId), + Domain: typeutil.ToPtr(zoneName), + Host: typeutil.ToPtr(subDomain), + Type: typeutil.ToPtr("TXT"), + LineCode: typeutil.ToPtr("Default"), + Value: typeutil.ToPtr(value), + State: typeutil.ToPtr(int32(1)), + TTL: typeutil.ToPtr(int32(d.config.TTL)), + } + _, err := d.client.UpdateRecord(request) + return err + } +} + +func (d *DNSProvider) removeDNSRecord(zoneName, subDomain string) error { + recordId, err := d.findDNSRecordId(zoneName, subDomain) + if err != nil { + return err + } + + if recordId == 0 { + return nil + } else { + // 删除解析记录 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11262&data=181&isNormal=1&vid=259 + request := &ctyundns.DeleteRecordRequest{ + RecordId: typeutil.ToPtr(recordId), + } + _, err = d.client.DeleteRecord(request) + return err + } +} diff --git a/internal/pkg/sdk3rd/ctyun/dns/api_add_record.go b/internal/pkg/sdk3rd/ctyun/dns/api_add_record.go new file mode 100644 index 00000000..8bcdcb21 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/dns/api_add_record.go @@ -0,0 +1,46 @@ +package dns + +import ( + "context" + "net/http" +) + +type AddRecordRequest struct { + Domain *string `json:"domain,omitempty"` + Host *string `json:"host,omitempty"` + Type *string `json:"type,omitempty"` + LineCode *string `json:"lineCode,omitempty"` + Value *string `json:"value,omitempty"` + TTL *int32 `json:"ttl,omitempty"` + State *int32 `json:"state,omitempty"` + Remark *string `json:"remark"` +} + +type AddRecordResponse struct { + baseResult + + ReturnObj *struct { + RecordId int32 `json:"recordId"` + } `json:"returnObj,omitempty"` +} + +func (c *Client) AddRecord(req *AddRecordRequest) (*AddRecordResponse, error) { + return c.AddRecordWithContext(context.Background(), req) +} + +func (c *Client) AddRecordWithContext(ctx context.Context, req *AddRecordRequest) (*AddRecordResponse, error) { + request, err := c.newRequest(http.MethodPost, "/v2/addRecord") + if err != nil { + return nil, err + } else { + request.SetContext(ctx) + request.SetBody(req) + } + + result := &AddRecordResponse{} + if _, err := c.doRequestWithResult(request, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/dns/api_delete_record.go b/internal/pkg/sdk3rd/ctyun/dns/api_delete_record.go new file mode 100644 index 00000000..679e6fcf --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/dns/api_delete_record.go @@ -0,0 +1,35 @@ +package dns + +import ( + "context" + "net/http" +) + +type DeleteRecordRequest struct { + RecordId *int32 `json:"recordId,omitempty"` +} + +type DeleteRecordResponse struct { + baseResult +} + +func (c *Client) DeleteRecord(req *DeleteRecordRequest) (*DeleteRecordResponse, error) { + return c.DeleteRecordWithContext(context.Background(), req) +} + +func (c *Client) DeleteRecordWithContext(ctx context.Context, req *DeleteRecordRequest) (*DeleteRecordResponse, error) { + request, err := c.newRequest(http.MethodPost, "/v2/deleteRecord") + if err != nil { + return nil, err + } else { + request.SetContext(ctx) + request.SetBody(req) + } + + result := &DeleteRecordResponse{} + if _, err := c.doRequestWithResult(request, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/dns/api_query_record_list.go b/internal/pkg/sdk3rd/ctyun/dns/api_query_record_list.go new file mode 100644 index 00000000..4d7f1617 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/dns/api_query_record_list.go @@ -0,0 +1,53 @@ +package dns + +import ( + "context" + "net/http" +) + +type QueryRecordListRequest struct { + Domain *string `json:"domain,omitempty"` + Host *string `json:"host,omitempty"` + Type *string `json:"type,omitempty"` + LineCode *string `json:"lineCode,omitempty"` + Value *string `json:"value,omitempty"` + State *int32 `json:"state,omitempty"` +} + +type QueryRecordListResponse struct { + baseResult + + ReturnObj *struct { + Records []*struct { + RecordId int32 `json:"recordId"` + Host string `json:"host"` + Type string `json:"type"` + LineCode string `json:"lineCode"` + Value string `json:"value"` + TTL int32 `json:"ttl"` + State int32 `json:"state"` + Remark string `json:"remark"` + } `json:"records,omitempty"` + } `json:"returnObj,omitempty"` +} + +func (c *Client) QueryRecordList(req *QueryRecordListRequest) (*QueryRecordListResponse, error) { + return c.QueryRecordListWithContext(context.Background(), req) +} + +func (c *Client) QueryRecordListWithContext(ctx context.Context, req *QueryRecordListRequest) (*QueryRecordListResponse, error) { + request, err := c.newRequest(http.MethodGet, "/v2/queryRecordList") + if err != nil { + return nil, err + } else { + request.SetContext(ctx) + request.SetBody(req) + } + + result := &QueryRecordListResponse{} + if _, err := c.doRequestWithResult(request, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/dns/api_update_record.go b/internal/pkg/sdk3rd/ctyun/dns/api_update_record.go new file mode 100644 index 00000000..ca827ce3 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/dns/api_update_record.go @@ -0,0 +1,47 @@ +package dns + +import ( + "context" + "net/http" +) + +type UpdateRecordRequest struct { + RecordId *int32 `json:"recordId,omitempty"` + Domain *string `json:"domain,omitempty"` + Host *string `json:"host,omitempty"` + Type *string `json:"type,omitempty"` + LineCode *string `json:"lineCode,omitempty"` + Value *string `json:"value,omitempty"` + TTL *int32 `json:"ttl,omitempty"` + State *int32 `json:"state,omitempty"` + Remark *string `json:"remark"` +} + +type UpdateRecordResponse struct { + baseResult + + ReturnObj *struct { + RecordId int32 `json:"recordId"` + } `json:"returnObj,omitempty"` +} + +func (c *Client) UpdateRecord(req *UpdateRecordRequest) (*UpdateRecordResponse, error) { + return c.UpdateRecordWithContext(context.Background(), req) +} + +func (c *Client) UpdateRecordWithContext(ctx context.Context, req *UpdateRecordRequest) (*UpdateRecordResponse, error) { + request, err := c.newRequest(http.MethodPost, "/v2/updateRecord") + if err != nil { + return nil, err + } else { + request.SetContext(ctx) + request.SetBody(req) + } + + result := &UpdateRecordResponse{} + if _, err := c.doRequestWithResult(request, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/dns/client.go b/internal/pkg/sdk3rd/ctyun/dns/client.go new file mode 100644 index 00000000..c2f2b594 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/dns/client.go @@ -0,0 +1,40 @@ +package dns + +import ( + "time" + + "github.com/go-resty/resty/v2" + "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi" +) + +const endpoint = "https://smartdns-global.ctapi.ctyun.cn" + +type Client struct { + client *openapi.Client +} + +func NewClient(accessKeyId, secretAccessKey string) (*Client, error) { + client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey) + if err != nil { + return nil, err + } + + return &Client{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) { + return c.client.NewRequest(method, path) +} + +func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) { + return c.client.DoRequest(request) +} + +func (c *Client) doRequestWithResult(request *resty.Request, result any) (*resty.Response, error) { + return c.client.DoRequestWithResult(request, result) +} diff --git a/internal/pkg/sdk3rd/ctyun/dns/types.go b/internal/pkg/sdk3rd/ctyun/dns/types.go new file mode 100644 index 00000000..8fe2eabd --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/dns/types.go @@ -0,0 +1,11 @@ +package dns + +import "encoding/json" + +type baseResult struct { + StatusCode json.RawMessage `json:"statusCode,omitempty"` + Message *string `json:"message,omitempty"` + Error *string `json:"error,omitempty"` + ErrorMessage *string `json:"errorMessage,omitempty"` + RequestId *string `json:"requestId,omitempty"` +} diff --git a/internal/pkg/sdk3rd/ctyun/openapi/client.go b/internal/pkg/sdk3rd/ctyun/openapi/client.go new file mode 100644 index 00000000..6960c83c --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/openapi/client.go @@ -0,0 +1,167 @@ +package openapi + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-resty/resty/v2" + "github.com/google/uuid" +) + +type Client struct { + client *resty.Client +} + +func NewClient(endpoint, accessKeyId, secretAccessKey string) (*Client, error) { + if endpoint == "" { + return nil, fmt.Errorf("sdk error: unset endpoint") + } + if _, err := url.Parse(endpoint); err != nil { + return nil, fmt.Errorf("sdk error: invalid endpoint: %w", err) + } + if accessKeyId == "" { + return nil, fmt.Errorf("sdk error: unset accessKey") + } + if secretAccessKey == "" { + return nil, fmt.Errorf("sdk error: unset secretKey") + } + + client := resty.New(). + SetBaseURL(endpoint). + SetHeader("Accept", "application/json"). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). + SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + // 生成时间戳及流水号 + now := time.Now() + eopDate := now.Format("20060102T150405Z") + eopReqId := uuid.New().String() + + // 获取查询参数 + queryStr := "" + if req.URL != nil { + queryStr = req.URL.Query().Encode() + } + + // 获取请求正文 + payloadStr := "" + if req.Body != nil { + reader, err := req.GetBody() + if err != nil { + return err + } + + defer reader.Close() + payload, err := io.ReadAll(reader) + if err != nil { + return err + } + + payloadStr = string(payload) + } + + // 构造代签字符串 + payloadHash := sha256.Sum256([]byte(payloadStr)) + payloadHashHex := hex.EncodeToString(payloadHash[:]) + dataToSign := fmt.Sprintf("ctyun-eop-request-id:%s\neop-date:%s\n\n%s\n%s", eopReqId, eopDate, queryStr, payloadHashHex) + + // 生成 ktime + hasher := hmac.New(sha256.New, []byte(secretAccessKey)) + hasher.Write([]byte(eopDate)) + ktime := hasher.Sum(nil) + + // 生成 kak + hasher = hmac.New(sha256.New, ktime) + hasher.Write([]byte(accessKeyId)) + kak := hasher.Sum(nil) + + // 生成 kdata + hasher = hmac.New(sha256.New, kak) + hasher.Write([]byte(now.Format("20060102"))) + kdate := hasher.Sum(nil) + + // 构造签名 + hasher = hmac.New(sha256.New, kdate) + hasher.Write([]byte(dataToSign)) + sign := hasher.Sum(nil) + signStr := base64.StdEncoding.EncodeToString(sign) + + // 设置请求头 + req.Header.Set("ctyun-eop-request-id", eopReqId) + req.Header.Set("eop-date", eopDate) + req.Header.Set("eop-authorization", fmt.Sprintf("%s Headers=ctyun-eop-request-id;eop-date Signature=%s", accessKeyId, signStr)) + + return nil + }) + + return &Client{ + 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("sdk error: unset method") + } + if path == "" { + return nil, fmt.Errorf("sdk error: unset path") + } + + req := c.client.R() + req.Method = method + req.URL = path + return req, nil +} + +func (c *Client) DoRequest(request *resty.Request) (*resty.Response, error) { + if request == nil { + return nil, fmt.Errorf("sdk error: nil request") + } + + // WARN: + // PLEASE DO NOT USE `req.SetResult` or `req.SetError` here. + + resp, err := request.Send() + if err != nil { + return resp, fmt.Errorf("sdk error: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdk error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) DoRequestWithResult(request *resty.Request, result any) (*resty.Response, error) { + if request == nil { + return nil, fmt.Errorf("sdk error: nil request") + } + + response, err := c.DoRequest(request) + if err != nil { + if response != nil { + json.Unmarshal(response.Body(), &result) + } + return response, err + } + + if len(response.Body()) != 0 { + if err := json.Unmarshal(response.Body(), &result); err != nil { + return response, fmt.Errorf("sdk error: failed to unmarshal response: %w", err) + } + } + + return response, nil +} diff --git a/ui/public/imgs/providers/ctcccloud.svg b/ui/public/imgs/providers/ctcccloud.svg new file mode 100644 index 00000000..b5ea5d76 --- /dev/null +++ b/ui/public/imgs/providers/ctcccloud.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 7b7640d3..91d7139f 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -30,6 +30,7 @@ import AccessFormCloudflareConfig from "./AccessFormCloudflareConfig"; import AccessFormClouDNSConfig from "./AccessFormClouDNSConfig"; import AccessFormCMCCCloudConfig from "./AccessFormCMCCCloudConfig"; import AccessFormConstellixConfig from "./AccessFormConstellixConfig"; +import AccessFormCTCCCloudConfig from "./AccessFormCTCCCloudConfig"; import AccessFormDeSECConfig from "./AccessFormDeSECConfig"; import AccessFormDigitalOceanConfig from "./AccessFormDigitalOceanConfig"; import AccessFormDingTalkBotConfig from "./AccessFormDingTalkBotConfig"; @@ -225,6 +226,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.CONSTELLIX: return ; + case ACCESS_PROVIDERS.CTCCCLOUD: + return ; case ACCESS_PROVIDERS.DESEC: return ; case ACCESS_PROVIDERS.DIGITALOCEAN: diff --git a/ui/src/components/access/AccessFormCTCCCloudConfig.tsx b/ui/src/components/access/AccessFormCTCCCloudConfig.tsx new file mode 100644 index 00000000..f0e9df39 --- /dev/null +++ b/ui/src/components/access/AccessFormCTCCCloudConfig.tsx @@ -0,0 +1,73 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; +import { type AccessConfigForCTCCCloud } from "@/domain/access"; + +type AccessFormCTCCCloudConfigFieldValues = Nullish; + +export type AccessFormCTCCCloudConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormCTCCCloudConfigFieldValues; + onValuesChange?: (values: AccessFormCTCCCloudConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormCTCCCloudConfigFieldValues => { + return { + accessKeyId: "", + secretAccessKey: "", + }; +}; + +const AccessFormCTCCCloudConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange: onValuesChange }: AccessFormCTCCCloudConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .min(1, t("access.form.ctcccloud_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretAccessKey: z + .string() + .min(1, t("access.form.ctcccloud_secret_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessFormCTCCCloudConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index cdf01c89..a2dbde73 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -25,6 +25,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForClouDNS | AccessConfigForCMCCCloud | AccessConfigForConstellix + | AccessConfigForCTCCCloud | AccessConfigForDeSEC | AccessConfigForDigitalOcean | AccessConfigForDingTalkBot @@ -185,6 +186,11 @@ export type AccessConfigForConstellix = { secretKey: string; }; +export type AccessConfigForCTCCCloud = { + accessKeyId: string; + secretAccessKey: string; +}; + export type AccessConfigForDeSEC = { token: string; }; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 9956a8c9..212ded80 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -24,6 +24,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ CLOUDNS: "cloudns", CMCCCLOUD: "cmcccloud", CONSTELLIX: "constellix", + CTCCCLOUD: "ctcccloud", DESEC: "desec", DIGITALOCEAN: "digitalocean", DINGTALKBOT: "dingtalkbot", @@ -164,6 +165,7 @@ export const accessProvidersMap: Maphttps://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key", + "access.form.ctcccloud_access_key_id.label": "CTCC StateCloud AccessKeyId", + "access.form.ctcccloud_access_key_id.placeholder": "Please enter CTCC StateCloud AccessKeyId", + "access.form.ctcccloud_access_key_id.tooltip": "For more information, see https://www.ctyun.cn/document/10015882/10015953", + "access.form.ctcccloud_secret_access_key.label": "CTCC StateCloud SecretAccessKey", + "access.form.ctcccloud_secret_access_key.placeholder": "Please enter CTCC StateCloud SecretAccessKey", + "access.form.ctcccloud_secret_access_key.tooltip": "For more information, see https://www.ctyun.cn/document/10015882/10015953", "access.form.desec_token.label": "deSEC token", "access.form.desec_token.placeholder": "Please enter deSEC token", "access.form.desec_token.tooltip": "For more information, see https://desec.readthedocs.io/en/latest/auth/tokens.html", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index f72c0ca1..a9761ae2 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -13,7 +13,7 @@ "provider.aliyun.clb": "Alibaba Cloud - CLB (Classic Load Balancer)", "provider.aliyun.dcdn": "Alibaba Cloud - DCDN (Dynamic Route for Content Delivery Network)", "provider.aliyun.ddos": "Alibaba Cloud - Anti-DDoS Proxy", - "provider.aliyun.dns": "Alibaba Cloud - DNS (Domain Name Service)", + "provider.aliyun.dns": "Alibaba Cloud - DNS", "provider.aliyun.esa": "Alibaba Cloud - ESA (Edge Security Acceleration)", "provider.aliyun.fc": "Alibaba Cloud - FC (Function Compute)", "provider.aliyun.ga": "Alibaba Cloud - GA (Global Accelerator)", @@ -38,7 +38,7 @@ "provider.baiducloud.blb": "Baidu Cloud - BLB (Load Balancer)", "provider.baiducloud.cdn": "Baidu Cloud - CDN (Content Delivery Network)", "provider.baiducloud.cert_upload": "Baidu Cloud - Upload to SSL Certificate Service", - "provider.baiducloud.dns": "Baidu Cloud - DNS (Domain Name Service)", + "provider.baiducloud.dns": "Baidu Cloud - DNS", "provider.baishan": "Baishan", "provider.baishan.cdn": "Baishan - CDN (Content Delivery Network)", "provider.baotapanel": "aaPanel (aka BaoTaPanel)", @@ -57,8 +57,10 @@ "provider.cloudflare": "Cloudflare", "provider.cloudns": "ClouDNS", "provider.cmcccloud": "China Mobile Cloud (ECloud)", + "provider.cmcccloud.dns": "China Mobile Cloud (ECloud) - DNS", "provider.constellix": "Constellix", - "provider.ctcccloud": "China Telecom Cloud (State Cloud)", + "provider.ctcccloud": "China Telecom Cloud (StateCloud)", + "provider.ctcccloud.smartdns": "China Telecom Cloud (StateCloud) - Smart DNS", "provider.cucccloud": "China Unicom Cloud", "provider.desec": "deSEC", "provider.digitalocean": "DigitalOcean", @@ -83,7 +85,7 @@ "provider.hetzner": "Hetzner", "provider.huaweicloud": "Huawei Cloud", "provider.huaweicloud.cdn": "Huawei Cloud - CDN (Content Delivery Network)", - "provider.huaweicloud.dns": "Huawei Cloud - DNS (Domain Name Service)", + "provider.huaweicloud.dns": "Huawei Cloud - DNS", "provider.huaweicloud.elb": "Huawei Cloud - ELB (Elastic Load Balance)", "provider.huaweicloud.scm_upload": "Huawei Cloud - Upload to SCM (SSL Certificate Manager)", "provider.huaweicloud.waf": "Huawei Cloud - WAF (Web Application Firewall)", @@ -130,7 +132,7 @@ "provider.tencentcloud.clb": "Tencent Cloud - CLB (Cloud Load Balancer)", "provider.tencentcloud.cos": "Tencent Cloud - COS (Cloud Object Storage)", "provider.tencentcloud.css": "Tencent Cloud - CSS (Cloud Streaming Service)", - "provider.tencentcloud.dns": "Tencent Cloud - DNS (Domain Name Service)", + "provider.tencentcloud.dns": "Tencent Cloud - DNS", "provider.tencentcloud.ecdn": "Tencent Cloud - ECDN (Enterprise Content Delivery Network)", "provider.tencentcloud.eo": "Tencent Cloud - EdgeOne", "provider.tencentcloud.gaap": "Tencent Cloud - GAAP (Global Application Acceleration Platform)", @@ -155,7 +157,7 @@ "provider.volcengine.certcenter_upload": "Volcengine - Upload to Certificate Center", "provider.volcengine.clb": "Volcengine - CLB (Cloud Load Balancer)", "provider.volcengine.dcdn": "Volcengine - DCDN (Dynamic Content Delivery Network)", - "provider.volcengine.dns": "Volcengine - DNS (Domain Name Service)", + "provider.volcengine.dns": "Volcengine - DNS", "provider.volcengine.imagex": "Volcengine - ImageX", "provider.volcengine.live": "Volcengine - Live", "provider.volcengine.tos": "Volcengine - TOS (Tinder Object Storage)", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index f5a9f3d8..a3852926 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -157,6 +157,12 @@ "access.form.constellix_secret_key.label": "Constellix Secret Key", "access.form.constellix_secret_key.placeholder": "请输入 Constellix Secret Key", "access.form.constellix_secret_key.tooltip": "这是什么?请参阅 https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key", + "access.form.ctcccloud_access_key_id.label": "天翼云 AccessKeyId", + "access.form.ctcccloud_access_key_id.placeholder": "请输入天翼云 AccessKeyId", + "access.form.ctcccloud_access_key_id.tooltip": "这是什么?请参阅 https://www.ctyun.cn/document/10015882/10015953", + "access.form.ctcccloud_secret_access_key.label": "天翼云 SecretAccessKey", + "access.form.ctcccloud_secret_access_key.placeholder": "请输入天翼云 SecretAccessKey", + "access.form.ctcccloud_secret_access_key.tooltip": "这是什么?请参阅 https://www.ctyun.cn/document/10015882/10015953", "access.form.desec_token.label": "deSEC Token", "access.form.desec_token.placeholder": "请输入 deSEC Token", "access.form.desec_token.tooltip": "这是什么?请参阅 https://desec.readthedocs.io/en/latest/auth/tokens.html", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index bf542b34..f021a586 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -57,9 +57,11 @@ "provider.cloudflare": "Cloudflare", "provider.cloudns": "ClouDNS", "provider.cmcccloud": "移动云", + "provider.cmcccloud.dns": "移动云 - 云解析 DNS", "provider.constellix": "Constellix", - "provider.ctcccloud": "联通云", - "provider.cucccloud": "天翼云", + "provider.ctcccloud": "天翼云", + "provider.ctcccloud.smartdns": "天翼云 - 智能 DNS", + "provider.cucccloud": "联通云", "provider.desec": "deSEC", "provider.digitalocean": "DigitalOcean", "provider.dingtalkbot": "钉钉群机器人",