feat(provider): new acme dns-01 provider: byteplus

This commit is contained in:
Fu Diwei
2026-06-04 01:14:21 +08:00
committed by RHQYZ
parent a7ab5db2aa
commit 473408cedd
7 changed files with 255 additions and 1 deletions
@@ -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)
}
+2
View File
@@ -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]
@@ -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
}
@@ -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)
}
+3 -1
View File
@@ -174,6 +174,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.AWS, "provider.aws", "/imgs/providers/aws.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.AZURE, "provider.azure", "/imgs/providers/azure.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.BUNNY, "provider.bunny", "/imgs/providers/bunny.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.BYTEPLUS, "provider.byteplus", "/imgs/providers/byteplus.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.GCORE, "provider.gcore", "/imgs/providers/gcore.png", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.NETLIFY, "provider.netlify", "/imgs/providers/netlify.png", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.VERCEL, "provider.vercel", "/imgs/providers/vercel.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
@@ -188,7 +189,6 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.WANGSU, "provider.wangsu", "/imgs/providers/wangsu.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.DOGECLOUD, "provider.dogecloud", "/imgs/providers/dogecloud.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.KSYUN, "provider.ksyun", "/imgs/providers/ksyun.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.BYTEPLUS, "provider.byteplus", "/imgs/providers/byteplus.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.CACHEFLY, "provider.cachefly", "/imgs/providers/cachefly.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.MOHUA, "provider.mohua", "/imgs/providers/mohua.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.UNICLOUD, "provider.unicloud", "/imgs/providers/unicloud.png", [ACCESS_USAGES.HOSTING]],
@@ -380,6 +380,8 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({
BEGET: `${ACCESS_PROVIDERS.BEGET}`,
BOOKMYNAME: `${ACCESS_PROVIDERS.BOOKMYNAME}`,
BUNNY: `${ACCESS_PROVIDERS.BUNNY}`,
BYTEPLUS: `${ACCESS_PROVIDERS.BYTEPLUS}`, // 兼容旧值,等同于 `BYTEPLUS_DNS`
BYTEPLUS_DNS: `${ACCESS_PROVIDERS.BYTEPLUS}-dns`,
CLOUDFLARE: `${ACCESS_PROVIDERS.CLOUDFLARE}`,
CLOUDNS: `${ACCESS_PROVIDERS.CLOUDNS}`,
CMCCCLOUD: `${ACCESS_PROVIDERS.CMCCCLOUD}`, // 兼容旧值,等同于 `CMCCCLOUD_DNS`
@@ -90,6 +90,7 @@
"bunny_cdn": "Bunny - CDN (Content Delivery Network)",
"byteplus": "BytePlus",
"byteplus_cdn": "BytePlus - CDN (Content Delivery Network)",
"byteplus_dns": "BytePlus - DNS",
"cachefly": "CacheFly",
"cdnfly": "Cdnfly",
"cloudflare": "Cloudflare",
@@ -90,6 +90,7 @@
"bunny_cdn": "Bunny - 内容分发网络 CDN",
"byteplus": "BytePlus",
"byteplus_cdn": "BytePlus - 内容分发网络 CDN",
"byteplus_dns": "BytePlus - DNS",
"cachefly": "CacheFly",
"cdnfly": "Cdnfly",
"cloudflare": "Cloudflare",