mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
feat(provider): new acme dns-01 provider: byteplus
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user