feat: new acme dns-01 provider: ucloud udnr

This commit is contained in:
Fu Diwei 2025-06-03 16:51:33 +08:00
parent 7210f63884
commit 6dc65eea2f
17 changed files with 387 additions and 20 deletions

View File

@ -39,6 +39,7 @@ import (
pRainYun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/rainyun"
pTencentCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud"
pTencentCloudEO "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo"
pUCloudUDNR "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ucloud-udnr"
pVercel "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/vercel"
pVolcEngine "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/volcengine"
pWestcn "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/westcn"
@ -596,6 +597,22 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
}
}
case domain.ACMEDns01ProviderTypeUCloudUDNR:
{
access := domain.AccessConfigForUCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pUCloudUDNR.NewChallengeProvider(&pUCloudUDNR.ChallengeProviderConfig{
PrivateKey: access.PrivateKey,
PublicKey: access.PublicKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeVercel:
{
access := domain.AccessConfigForVercel{}

View File

@ -158,6 +158,7 @@ const (
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")

View File

@ -1,9 +1,8 @@
package lego_aliyunesa
package internal
import (
"errors"
"fmt"
"strings"
"sync"
"time"
@ -102,13 +101,13 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return fmt.Errorf("alicloud-esa: could not find zone for domain %q: %w", domain, err)
}
siteName := strings.TrimRight(authZone, ".")
siteName := dns01.UnFqdn(authZone)
siteId, err := d.getSiteId(siteName)
if err != nil {
return fmt.Errorf("alicloud-esa: could not find site for zone %q: %w", siteName, err)
}
if err := d.addOrUpdateDNSRecord(siteId, strings.TrimRight(info.EffectiveFQDN, "."), info.Value); err != nil {
if err := d.addOrUpdateDNSRecord(siteId, dns01.UnFqdn(info.EffectiveFQDN), info.Value); err != nil {
return fmt.Errorf("alicloud-esa: %w", err)
}
@ -123,13 +122,13 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return fmt.Errorf("alicloud-esa: could not find zone for domain %q: %w", domain, err)
}
siteName := strings.TrimRight(authZone, ".")
siteName := dns01.UnFqdn(authZone)
siteId, err := d.getSiteId(siteName)
if err != nil {
return fmt.Errorf("alicloud-esa: could not find site for zone %q: %w", siteName, err)
}
if err := d.removeDNSRecord(siteId, strings.TrimRight(info.EffectiveFQDN, ".")); err != nil {
if err := d.removeDNSRecord(siteId, dns01.UnFqdn(info.EffectiveFQDN)); err != nil {
return fmt.Errorf("alicloud-esa: %w", err)
}

View File

@ -1,4 +1,4 @@
package lego_baiducloud
package internal
import (
"errors"

View File

@ -1,4 +1,4 @@
package lego_dnsla
package internal
import (
"errors"

View File

@ -1,4 +1,4 @@
package lego_dynv6
package internal
import (
"context"

View File

@ -1,4 +1,4 @@
package lego_gname
package internal
import (
"errors"

View File

@ -1,4 +1,4 @@
package lego_jdcloud
package internal
import (
"errors"

View File

@ -1,10 +1,9 @@
package lego_tencentcloudeo
package internal
import (
"errors"
"fmt"
"math"
"strings"
"time"
"github.com/go-acme/lego/v4/challenge"
@ -91,7 +90,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
if err := d.addOrUpdateDNSRecord(strings.TrimRight(info.EffectiveFQDN, "."), info.Value); err != nil {
if err := d.addOrUpdateDNSRecord(dns01.UnFqdn(info.EffectiveFQDN), info.Value); err != nil {
return fmt.Errorf("tencentcloud-eo: %w", err)
}
@ -101,7 +100,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
if err := d.removeDNSRecord(strings.TrimRight(info.EffectiveFQDN, ".")); err != nil {
if err := d.removeDNSRecord(dns01.UnFqdn(info.EffectiveFQDN)); err != nil {
return fmt.Errorf("tencentcloud-eo: %w", err)
}

View File

@ -0,0 +1,165 @@
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"
"github.com/ucloud/ucloud-sdk-go/ucloud"
"github.com/ucloud/ucloud-sdk-go/ucloud/auth"
"github.com/usual2970/certimate/internal/pkg/sdk3rd/ucloud/udnr"
)
const (
envNamespace = "UCLOUDUDNR_"
EnvPublicKey = envNamespace + "PUBLIC_KEY"
EnvPrivateKey = envNamespace + "PRIVATE_KEY"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
PrivateKey string
PublicKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int32
HTTPTimeout time.Duration
}
type DNSProvider struct {
client *udnr.UDNRClient
config *Config
}
func NewDefaultConfig() *Config {
return &Config{
TTL: int32(env.GetOrDefaultInt(EnvTTL, 300)),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvPrivateKey, EnvPublicKey)
if err != nil {
return nil, fmt.Errorf("ucloud-udnr: %w", err)
}
config := NewDefaultConfig()
config.PrivateKey = values[EnvPrivateKey]
config.PublicKey = values[EnvPublicKey]
return NewDNSProviderConfig(config)
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("ucloud-udnr: the configuration of the DNS provider is nil")
}
cfg := ucloud.NewConfig()
credential := auth.NewCredential()
credential.PrivateKey = config.PrivateKey
credential.PublicKey = config.PublicKey
client := udnr.NewClient(&cfg, &credential)
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("ucloud-udnr: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("ucloud-udnr: %w", err)
}
udnrDomainDNSQueryReq := d.client.NewQueryDomainDNSRequest()
udnrDomainDNSQueryReq.Dn = ucloud.String(authZone)
if udnrDomainDNSQueryResp, err := d.client.QueryDomainDNS(udnrDomainDNSQueryReq); err != nil {
return fmt.Errorf("ucloud-udnr: %w", err)
} else {
for _, record := range udnrDomainDNSQueryResp.Data {
if record.DnsType == "TXT" && record.RecordName == subDomain {
udnrDomainDNSDeleteReq := d.client.NewDeleteDomainDNSRequest()
udnrDomainDNSDeleteReq.Dn = ucloud.String(authZone)
udnrDomainDNSDeleteReq.DnsType = ucloud.String(record.DnsType)
udnrDomainDNSDeleteReq.RecordName = ucloud.String(record.RecordName)
udnrDomainDNSDeleteReq.Content = ucloud.String(record.Content)
d.client.DeleteDomainDNS(udnrDomainDNSDeleteReq)
break
}
}
}
udnrDomainDNSAddReq := d.client.NewAddDomainDNSRequest()
udnrDomainDNSAddReq.Dn = ucloud.String(authZone)
udnrDomainDNSAddReq.DnsType = ucloud.String("TXT")
udnrDomainDNSAddReq.RecordName = ucloud.String(subDomain)
udnrDomainDNSAddReq.Content = ucloud.String(info.Value)
udnrDomainDNSAddReq.TTL = ucloud.Int(int(d.config.TTL))
if _, err := d.client.AddDomainDNS(udnrDomainDNSAddReq); err != nil {
return fmt.Errorf("ucloud-udnr: %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("ucloud-udnr: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("ucloud-udnr: %w", err)
}
udnrDomainDNSQueryReq := d.client.NewQueryDomainDNSRequest()
udnrDomainDNSQueryReq.Dn = ucloud.String(authZone)
if udnrDomainDNSQueryResp, err := d.client.QueryDomainDNS(udnrDomainDNSQueryReq); err != nil {
return fmt.Errorf("ucloud-udnr: %w", err)
} else {
for _, record := range udnrDomainDNSQueryResp.Data {
if record.DnsType == "TXT" && record.RecordName == subDomain {
udnrDomainDNSDeleteReq := d.client.NewDeleteDomainDNSRequest()
udnrDomainDNSDeleteReq.Dn = ucloud.String(authZone)
udnrDomainDNSDeleteReq.DnsType = ucloud.String(record.DnsType)
udnrDomainDNSDeleteReq.RecordName = ucloud.String(record.RecordName)
udnrDomainDNSDeleteReq.Content = ucloud.String(record.Content)
d.client.DeleteDomainDNS(udnrDomainDNSDeleteReq)
break
}
}
}
return nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}

View File

@ -0,0 +1,40 @@
package ucloududnr
import (
"errors"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ucloud-udnr/internal"
)
type ChallengeProviderConfig struct {
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey"`
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int32 `json:"dnsTTL,omitempty"`
}
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
if config == nil {
return nil, errors.New("config is nil")
}
providerConfig := internal.NewDefaultConfig()
providerConfig.PrivateKey = config.PrivateKey
providerConfig.PublicKey = config.PublicKey
if config.DnsTTL != 0 {
providerConfig.TTL = 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,115 @@
package udnr
import (
"github.com/ucloud/ucloud-sdk-go/ucloud/request"
"github.com/ucloud/ucloud-sdk-go/ucloud/response"
)
type QueryDomainDNSRequest struct {
request.CommonBase
Dn *string `required:"true"`
}
type QueryDomainDNSResponse struct {
response.CommonBase
Data []DomainDNSRecord
}
func (c *UDNRClient) NewQueryDomainDNSRequest() *QueryDomainDNSRequest {
req := &QueryDomainDNSRequest{}
c.Client.SetupRequest(req)
req.SetRetryable(false)
return req
}
func (c *UDNRClient) QueryDomainDNS(req *QueryDomainDNSRequest) (*QueryDomainDNSResponse, error) {
var err error
var res QueryDomainDNSResponse
reqCopier := *req
err = c.Client.InvokeAction("UdnrDomainDNSQuery", &reqCopier, &res)
if err != nil {
return &res, err
}
return &res, nil
}
type AddDomainDNSRequest struct {
request.CommonBase
Dn *string `required:"true"`
DnsType *string `required:"true"`
RecordName *string `required:"true"`
Content *string `required:"true"`
TTL *int `required:"true"`
Prio *int `required:"false"`
}
type AddDomainDNSResponse struct {
response.CommonBase
}
func (c *UDNRClient) NewAddDomainDNSRequest() *AddDomainDNSRequest {
req := &AddDomainDNSRequest{}
c.Client.SetupRequest(req)
req.SetRetryable(false)
return req
}
func (c *UDNRClient) AddDomainDNS(req *AddDomainDNSRequest) (*AddDomainDNSResponse, error) {
var err error
var res AddDomainDNSResponse
reqCopier := *req
err = c.Client.InvokeAction("UdnrDomainDNSAdd", &reqCopier, &res)
if err != nil {
return &res, err
}
return &res, nil
}
type DeleteDomainDNSRequest struct {
request.CommonBase
Dn *string `required:"true"`
DnsType *string `required:"true"`
RecordName *string `required:"true"`
Content *string `required:"true"`
}
type DeleteDomainDNSResponse struct {
response.CommonBase
}
func (c *UDNRClient) NewDeleteDomainDNSRequest() *DeleteDomainDNSRequest {
req := &DeleteDomainDNSRequest{}
c.Client.SetupRequest(req)
req.SetRetryable(false)
return req
}
func (c *UDNRClient) DeleteDomainDNS(req *DeleteDomainDNSRequest) (*DeleteDomainDNSResponse, error) {
var err error
var res DeleteDomainDNSResponse
reqCopier := *req
err = c.Client.InvokeAction("UdnrDeleteDnsRecord", &reqCopier, &res)
if err != nil {
return &res, err
}
return &res, nil
}

View File

@ -0,0 +1,18 @@
package udnr
import (
"github.com/ucloud/ucloud-sdk-go/ucloud"
"github.com/ucloud/ucloud-sdk-go/ucloud/auth"
)
type UDNRClient struct {
*ucloud.Client
}
func NewClient(config *ucloud.Config, credential *auth.Credential) *UDNRClient {
meta := ucloud.ClientMeta{Product: "UDNR"}
client := ucloud.NewClientWithMeta(config, credential, meta)
return &UDNRClient{
client,
}
}

View File

@ -0,0 +1,9 @@
package udnr
type DomainDNSRecord struct {
DnsType string
RecordName string
Content string
TTL int
Prio int
}

View File

@ -121,6 +121,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[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.RAINYUN, "provider.rainyun", "/imgs/providers/rainyun.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.UCLOUD, "provider.ucloud", "/imgs/providers/ucloud.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.QINIU, "provider.qiniu", "/imgs/providers/qiniu.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.UPYUN, "provider.upyun", "/imgs/providers/upyun.svg", [ACCESS_USAGES.HOSTING]],
@ -128,7 +129,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.BYTEPLUS, "provider.byteplus", "/imgs/providers/byteplus.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.UCLOUD, "provider.ucloud", "/imgs/providers/ucloud.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.UNICLOUD, "provider.unicloud", "/imgs/providers/unicloud.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS["1PANEL"], "provider.1panel", "/imgs/providers/1panel.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.BAOTAPANEL, "provider.baotapanel", "/imgs/providers/baotapanel.svg", [ACCESS_USAGES.HOSTING]],
@ -289,6 +289,7 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({
PORKBUN: `${ACCESS_PROVIDERS.PORKBUN}`,
POWERDNS: `${ACCESS_PROVIDERS.POWERDNS}`,
RAINYUN: `${ACCESS_PROVIDERS.RAINYUN}`,
UCLOUD_UDNR: `${ACCESS_PROVIDERS.UCLOUD}-udnr`,
TENCENTCLOUD: `${ACCESS_PROVIDERS.TENCENTCLOUD}`, // 兼容旧值,等同于 `TENCENTCLOUD_DNS`
TENCENTCLOUD_DNS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-dns`,
TENCENTCLOUD_EO: `${ACCESS_PROVIDERS.TENCENTCLOUD}-eo`,
@ -346,6 +347,7 @@ export const acmeDns01ProvidersMap: Map<ACMEDns01Provider["type"] | string, ACME
[ACME_DNS01_PROVIDERS.VERCEL, "provider.vercel"],
[ACME_DNS01_PROVIDERS.CMCCCLOUD, "provider.cmcccloud"],
[ACME_DNS01_PROVIDERS.RAINYUN, "provider.rainyun"],
[ACME_DNS01_PROVIDERS.UCLOUD_UDNR, "provider.ucloud.udnr"],
[ACME_DNS01_PROVIDERS.WESTCN, "provider.westcn"],
[ACME_DNS01_PROVIDERS.POWERDNS, "provider.powerdns"],
[ACME_DNS01_PROVIDERS.ACMEHTTPREQ, "provider.acmehttpreq"],

View File

@ -33,7 +33,7 @@
"provider.azure.keyvault": "Azure - KeyVault",
"provider.baiducloud": "Baidu Cloud",
"provider.baiducloud.appblb": "Baidu Cloud - AppBLB (Application Baidu Load Balancer)",
"provider.baiducloud.blb": "Baidu Cloud - BLB (Baidu Load Balancer)",
"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)",
@ -114,7 +114,7 @@
"provider.qiniu.kodo": "Qiniu - Kodo",
"provider.qiniu.pili": "Qiniu - Pili",
"provider.rainyun": "Rain Yun",
"provider.rainyun.rcdn": "Rain Yun - RCDN (Rain Content Delivery Network)",
"provider.rainyun.rcdn": "Rain Yun - RCDN (Content Delivery Network)",
"provider.ratpanel": "RatPanel",
"provider.ratpanel.console": "RatPanel - Console",
"provider.ratpanel.site": "RatPanel - Website",
@ -137,8 +137,9 @@
"provider.tencentcloud.vod": "Tencent Cloud - VOD (Video on Demand)",
"provider.tencentcloud.waf": "Tencent Cloud - WAF (Web Application Firewall)",
"provider.ucloud": "UCloud",
"provider.ucloud.ucdn": "UCloud - UCDN (UCloud Content Delivery Network)",
"provider.ucloud.us3": "UCloud - US3 (UCloud Object-based Storage)",
"provider.ucloud.ucdn": "UCloud - UCDN (Content Delivery Network)",
"provider.ucloud.udnr": "UCloud - UDNR (Domain Name Registrar)",
"provider.ucloud.us3": "UCloud - US3 (Object-based Storage)",
"provider.unicloud": "uniCloud (DCloud)",
"provider.unicloud.webhost": "uniCloud (DCloud) - Web Host",
"provider.upyun": "UPYUN",

View File

@ -138,6 +138,7 @@
"provider.tencentcloud.waf": "腾讯云 - Web 应用防火墙 WAF",
"provider.ucloud": "优刻得",
"provider.ucloud.ucdn": "优刻得 - 内容分发 UCDN",
"provider.ucloud.udnr": "优刻得 - 域名服务 UDNR",
"provider.ucloud.us3": "优刻得 - 对象存储 US3",
"provider.unicloud": "uniCloud (DCloud)",
"provider.unicloud.webhost": "uniCloud (DCloud) - 前端网页托管",