feat: new acme dns-01 provider: statecloud smartdns

This commit is contained in:
Fu Diwei 2025-06-12 23:05:18 +08:00
parent fb62f1e105
commit 9c8ab98efb
23 changed files with 837 additions and 65 deletions

View File

@ -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{}

View File

@ -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"`
}

View File

@ -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

View File

@ -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()

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M853.205 403.712A169.6 169.6 0 0 0 661.59 299.605a260.864 260.864 0 0 0-193.962-86.272 261.76 261.76 0 0 0-232.363 141.952h-0.427C105.26 355.285 0 461.184 0 591.957c0 130.688 105.216 236.715 234.88 236.715h368.768c3.157 0 4.693-3.84 2.39-6.059-25.558-25.173-46.251-55.466-60.545-89.088a5.803 5.803 0 0 0-5.29-3.584H234.965a137.045 137.045 0 0 1-131.968-100.437 138.667 138.667 0 0 1 56.96-153.173 135.723 135.723 0 0 1 139.862-5.803c4.693-82.432 67.285-152.32 151.466-162.347 78.038-9.301 149.888 35.584 178.859 105.088h0.043a85.163 85.163 0 0 1 106.24-18.432 86.656 86.656 0 0 1 40.021 100.95 121.813 121.813 0 0 1 144.47 70.357c23.722 55.85 1.791 122.71-50.134 153.643-59.477 35.413-135.68 14.08-168.747-46.464a122.496 122.496 0 0 1-13.866-42.24h60.544c3.968 0 5.973-4.822 3.157-7.68L635.179 505.899a5.547 5.547 0 0 0-7.894 0L510.72 623.403c-2.773 2.858-0.81 7.68 3.157 7.68h57.344c0.555 28.8 6.571 56.277 17.024 81.408a226.432 226.432 0 0 0 209.238 140.842c125.184 0 226.517-102.229 226.517-228.309a227.968 227.968 0 0 0-170.795-221.312" fill="#DF0629"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormCMCCCloudConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.CONSTELLIX:
return <AccessFormConstellixConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.CTCCCLOUD:
return <AccessFormCTCCCloudConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.DESEC:
return <AccessFormDeSECConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.DIGITALOCEAN:

View File

@ -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<AccessConfigForCTCCCloud>;
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<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="accessKeyId"
label={t("access.form.ctcccloud_access_key_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ctcccloud_access_key_id.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.ctcccloud_access_key_id.placeholder")} />
</Form.Item>
<Form.Item
name="secretAccessKey"
label={t("access.form.ctcccloud_secret_access_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ctcccloud_secret_access_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.ctcccloud_secret_access_key.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormCTCCCloudConfig;

View File

@ -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;
};

View File

@ -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: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.PORKBUN, "provider.porkbun", "/imgs/providers/porkbun.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.VERCEL, "provider.vercel", "/imgs/providers/vercel.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.CMCCCLOUD, "provider.cmcccloud", "/imgs/providers/cmcccloud.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.CTCCCLOUD, "provider.ctcccloud", "/imgs/providers/ctcccloud.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.WESTCN, "provider.westcn", "/imgs/providers/westcn.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.POWERDNS, "provider.powerdns", "/imgs/providers/powerdns.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.ACMEHTTPREQ, "provider.acmehttpreq", "/imgs/providers/acmehttpreq.svg", [ACCESS_USAGES.DNS]],
@ -267,8 +269,11 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({
BUNNY: `${ACCESS_PROVIDERS.BUNNY}`,
CLOUDFLARE: `${ACCESS_PROVIDERS.CLOUDFLARE}`,
CLOUDNS: `${ACCESS_PROVIDERS.CLOUDNS}`,
CMCCCLOUD: `${ACCESS_PROVIDERS.CMCCCLOUD}`,
CMCCCLOUD: `${ACCESS_PROVIDERS.CMCCCLOUD}`, // 兼容旧值,等同于 `CMCCCLOUD_DNS`
CMCCCLOUD_DNS: `${ACCESS_PROVIDERS.CMCCCLOUD}-dns`,
CONSTELLIX: `${ACCESS_PROVIDERS.CONSTELLIX}`,
CTCCCLOUD: `${ACCESS_PROVIDERS.CTCCCLOUD}`, // 兼容旧值,等同于 `CTCCCLOUD_SMARTDNS`
CTCCCLOUD_SMARTDNS: `${ACCESS_PROVIDERS.CTCCCLOUD}-smartdns`,
DESEC: `${ACCESS_PROVIDERS.DESEC}`,
DIGITALOCEAN: `${ACCESS_PROVIDERS.DIGITALOCEAN}`,
DNSLA: `${ACCESS_PROVIDERS.DNSLA}`,
@ -347,7 +352,8 @@ export const acmeDns01ProvidersMap: Map<ACMEDns01Provider["type"] | string, ACME
[ACME_DNS01_PROVIDERS.NS1, "provider.ns1"],
[ACME_DNS01_PROVIDERS.PORKBUN, "provider.porkbun"],
[ACME_DNS01_PROVIDERS.VERCEL, "provider.vercel"],
[ACME_DNS01_PROVIDERS.CMCCCLOUD, "provider.cmcccloud"],
[ACME_DNS01_PROVIDERS.CMCCCLOUD_DNS, "provider.cmcccloud.dns"],
[ACME_DNS01_PROVIDERS.CTCCCLOUD_SMARTDNS, "provider.ctcccloud.smartdns"],
[ACME_DNS01_PROVIDERS.RAINYUN, "provider.rainyun"],
[ACME_DNS01_PROVIDERS.UCLOUD_UDNR, "provider.ucloud.udnr"],
[ACME_DNS01_PROVIDERS.WESTCN, "provider.westcn"],

View File

@ -157,6 +157,12 @@
"access.form.constellix_secret_key.label": "Constellix API secret key",
"access.form.constellix_secret_key.placeholder": "Please enter Constellix API secret key",
"access.form.constellix_secret_key.tooltip": "For more information, see <a href=\"https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key\" target=\"_blank\">https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key</a>",
"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 <a href=\"https://www.ctyun.cn/document/10015882/10015953\" target=\"_blank\">https://www.ctyun.cn/document/10015882/10015953</a>",
"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 <a href=\"https://www.ctyun.cn/document/10015882/10015953\" target=\"_blank\">https://www.ctyun.cn/document/10015882/10015953</a>",
"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 <a href=\"https://desec.readthedocs.io/en/latest/auth/tokens.html#manage-tokens\" target=\"_blank\">https://desec.readthedocs.io/en/latest/auth/tokens.html</a>",

View File

@ -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)",

View File

@ -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": "这是什么?请参阅 <a href=\"https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key\" target=\"_blank\">https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key</a>",
"access.form.ctcccloud_access_key_id.label": "天翼云 AccessKeyId",
"access.form.ctcccloud_access_key_id.placeholder": "请输入天翼云 AccessKeyId",
"access.form.ctcccloud_access_key_id.tooltip": "这是什么?请参阅 <a href=\"https://www.ctyun.cn/document/10015882/10015953\" target=\"_blank\">https://www.ctyun.cn/document/10015882/10015953</a>",
"access.form.ctcccloud_secret_access_key.label": "天翼云 SecretAccessKey",
"access.form.ctcccloud_secret_access_key.placeholder": "请输入天翼云 SecretAccessKey",
"access.form.ctcccloud_secret_access_key.tooltip": "这是什么?请参阅 <a href=\"https://www.ctyun.cn/document/10015882/10015953\" target=\"_blank\">https://www.ctyun.cn/document/10015882/10015953</a>",
"access.form.desec_token.label": "deSEC Token",
"access.form.desec_token.placeholder": "请输入 deSEC Token",
"access.form.desec_token.tooltip": "这是什么?请参阅 <a href=\"https://desec.readthedocs.io/en/latest/auth/tokens.html#manage-tokens\" target=\"_blank\">https://desec.readthedocs.io/en/latest/auth/tokens.html</a>",

View File

@ -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": "钉钉群机器人",