From 473408cedd5538f7ce5415cf1579217f76652601 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 4 Jun 2026 01:08:20 +0800 Subject: [PATCH] feat(provider): new acme dns-01 provider: byteplus --- .../certacme/certifiers/sp_byteplus_dns.go | 29 +++ internal/domain/provider.go | 2 + .../challengers/dns01/byteplus/byteplus.go | 40 ++++ .../dns01/byteplus/internal/lego.go | 179 ++++++++++++++++++ ui/src/domain/provider.ts | 4 +- ui/src/i18n/resources/en/nls.provider.json | 1 + ui/src/i18n/resources/zh/nls.provider.json | 1 + 7 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 internal/certacme/certifiers/sp_byteplus_dns.go create mode 100644 pkg/core/certifier/challengers/dns01/byteplus/byteplus.go create mode 100644 pkg/core/certifier/challengers/dns01/byteplus/internal/lego.go diff --git a/internal/certacme/certifiers/sp_byteplus_dns.go b/internal/certacme/certifiers/sp_byteplus_dns.go new file mode 100644 index 000000000..5ff252f44 --- /dev/null +++ b/internal/certacme/certifiers/sp_byteplus_dns.go @@ -0,0 +1,29 @@ +package certifiers + +import ( + "fmt" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core" + "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/byteplus" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeBytePlusDNS, func(options *ProviderFactoryOptions) (core.ACMEChallenger, error) { + credentials := domain.AccessConfigForBytePlus{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := byteplus.NewChallenger(&byteplus.ChallengerConfig{ + AccessKey: credentials.AccessKey, + SecretKey: credentials.SecretKey, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return provider, err + }) + + ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeBytePlus, domain.ACMEDns01ProviderTypeBytePlusDNS) +} diff --git a/internal/domain/provider.go b/internal/domain/provider.go index d12487008..d889a82d5 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -203,6 +203,8 @@ const ( ACMEDns01ProviderTypeBeget = ACMEDns01ProviderType(AccessProviderTypeBeget) ACMEDns01ProviderTypeBookMyName = ACMEDns01ProviderType(AccessProviderTypeBookMyName) ACMEDns01ProviderTypeBunny = ACMEDns01ProviderType(AccessProviderTypeBunny) + ACMEDns01ProviderTypeBytePlus = ACMEDns01ProviderType(AccessProviderTypeBytePlus) // 兼容旧值,等同于 [ACMEDns01ProviderTypeBytePlusDNS] + ACMEDns01ProviderTypeBytePlusDNS = ACMEDns01ProviderType(AccessProviderTypeBytePlus + "-dns") ACMEDns01ProviderTypeCloudflare = ACMEDns01ProviderType(AccessProviderTypeCloudflare) ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS) ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCMCCCloudDNS] diff --git a/pkg/core/certifier/challengers/dns01/byteplus/byteplus.go b/pkg/core/certifier/challengers/dns01/byteplus/byteplus.go new file mode 100644 index 000000000..d315e1e8e --- /dev/null +++ b/pkg/core/certifier/challengers/dns01/byteplus/byteplus.go @@ -0,0 +1,40 @@ +package byteplus + +import ( + "fmt" + "time" + + "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/byteplus/internal" + + "github.com/certimate-go/certimate/pkg/core/certifier" +) + +type ChallengerConfig struct { + AccessKey string `json:"accessKey"` + SecretKey string `json:"secretKey"` + DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int `json:"dnsTTL,omitempty"` +} + +func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { + if config == nil { + return nil, fmt.Errorf("the configuration of the acme challenge provider is nil") + } + + providerConfig := internal.NewDefaultConfig() + providerConfig.AccessKey = config.AccessKey + providerConfig.SecretKey = config.SecretKey + 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/certifier/challengers/dns01/byteplus/internal/lego.go b/pkg/core/certifier/challengers/dns01/byteplus/internal/lego.go new file mode 100644 index 000000000..c23b4584a --- /dev/null +++ b/pkg/core/certifier/challengers/dns01/byteplus/internal/lego.go @@ -0,0 +1,179 @@ +package internal + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + bpdns "github.com/byteplus-sdk/byteplus-sdk-golang/service/dns" + "github.com/go-acme/lego/v5/challenge" + "github.com/go-acme/lego/v5/challenge/dns01" + "github.com/go-acme/lego/v5/platform/env" + "github.com/samber/lo" +) + +const ( + envNamespace = "BYTEPLUS_" + + EnvAccessKey = envNamespace + "ACCESSKEY" + EnvSecretKey = envNamespace + "SECRETKEY" + EnvRegion = envNamespace + "REGION" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +const defaultTTL = 600 + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +type Config struct { + AccessKey string + SecretKey string + + TTL int + PropagationTimeout time.Duration + PollingInterval time.Duration + HTTPTimeout time.Duration +} + +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 4*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, time.Duration(bpdns.Timeout)*time.Second), + } +} + +type DNSProvider struct { + client *bpdns.Client + config *Config + + recordIDs map[string]*string // Key: ChallengeToken; Value: RecordID + recordIDsMu sync.Mutex +} + +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAccessKey, EnvSecretKey) + if err != nil { + return nil, fmt.Errorf("byteplus: %w", err) + } + + config := NewDefaultConfig() + config.AccessKey = values[EnvAccessKey] + config.SecretKey = values[EnvSecretKey] + + return NewDNSProviderConfig(config) +} + +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("byteplus: the configuration of the DNS provider is nil") + } + + if config.AccessKey == "" || config.SecretKey == "" { + return nil, errors.New("byteplus: missing credentials") + } + + client := bpdns.InitDNSBytePlusClient() + client.SetAccessKey(config.AccessKey) + client.SetSecretKey(config.SecretKey) + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]*string), + }, nil +} + +func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(ctx, domain, keyAuth) + + zoneInfo, err := d.findZone(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("byteplus: get zone ID: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, lo.FromPtr(zoneInfo.ZoneName)) + if err != nil { + return fmt.Errorf("byteplus: %w", err) + } + + record, err := d.client.CreateRecord(ctx, &bpdns.CreateRecordRequest{ + Host: lo.ToPtr(subDomain), + TTL: lo.ToPtr(int64(d.config.TTL)), + Type: lo.ToPtr("TXT"), + Value: lo.ToPtr(info.Value), + ZID: zoneInfo.ZID, + }) + if err != nil { + return fmt.Errorf("byteplus: error when create record: %w", err) + } + + d.recordIDsMu.Lock() + d.recordIDs[token] = record.RecordID + d.recordIDsMu.Unlock() + + return nil +} + +func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(ctx, domain, keyAuth) + + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + if !ok { + return fmt.Errorf("byteplus: unknown record ID for '%s'", info.EffectiveFQDN) + } + + err := d.client.DeleteRecord(ctx, &bpdns.DeleteRecordRequest{ + RecordID: recordID, + }) + if err != nil { + return fmt.Errorf("byteplus: error when delete record: %w", err) + } + + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + + return nil +} + +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (bpdns.TopZoneResponse, error) { + for domain := range dns01.UnFqdnDomainsSeq(fqdn) { + lzr := &bpdns.ListZonesRequest{ + Key: lo.ToPtr(domain), + SearchMode: lo.ToPtr("exact"), + } + + zones, err := d.client.ListZones(ctx, lzr) + if err != nil { + return bpdns.TopZoneResponse{}, fmt.Errorf("list zones: %w", err) + } + + total := lo.FromPtr(zones.Total) + + if total == 0 || len(zones.Zones) == 0 { + continue + } + + if total > 1 { + return bpdns.TopZoneResponse{}, fmt.Errorf("too many zone for %s", domain) + } + + return zones.Zones[0], nil + } + + return bpdns.TopZoneResponse{}, fmt.Errorf("zone no found for fqdn: %s", fqdn) +} diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 3204c2335..93321162a 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -174,6 +174,7 @@ export const accessProvidersMap: Map