diff --git a/README.md b/README.md index bae31da1..f9d4df00 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ make local.run | :----------------------------------------------------------------- | :-------------------------------------- | | [阿里云](https://www.aliyun.com/) | | | [腾讯云](https://cloud.tencent.com/) | | +| [百度智能云](https://cloud.baidu.com/) | | | [华为云](https://www.huaweicloud.com/) | | | [火山引擎](https://www.volcengine.com/) | | | [AWS Route53](https://aws.amazon.com/route53/) | | diff --git a/README_EN.md b/README_EN.md index 0daf5b2b..a6d13c26 100644 --- a/README_EN.md +++ b/README_EN.md @@ -89,6 +89,7 @@ The following DNS providers are supported: | :----------------------------------------------------------- | :------------------------------------ | | [Alibaba Cloud](https://www.alibabacloud.com/) | | | [Tencent Cloud](https://www.tencentcloud.com/) | | +| [Baidu AI Cloud](https://intl.cloud.baidu.com/) | | | [Huawei Cloud](https://www.huaweicloud.com/) | | | [Volcengine](https://www.volcengine.com/) | | | [AWS Route53](https://aws.amazon.com/route53/) | | diff --git a/go.mod b/go.mod index ee281548..c07f2a5b 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aws/aws-sdk-go-v2/service/acm v1.30.18 github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.10 - github.com/baidubce/bce-sdk-go v0.9.216 + github.com/baidubce/bce-sdk-go v0.9.217 github.com/byteplus-sdk/byteplus-sdk-golang v1.0.41 github.com/go-acme/lego/v4 v4.21.0 github.com/go-resty/resty/v2 v2.16.5 diff --git a/go.sum b/go.sum index c3d1b3a6..f127aeb4 100644 --- a/go.sum +++ b/go.sum @@ -261,6 +261,8 @@ github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/baidubce/bce-sdk-go v0.9.216 h1:jRq4C1UGYcvHo6Gst2kuUzhWwJM6EqXCmhIsTKQvf4k= github.com/baidubce/bce-sdk-go v0.9.216/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= +github.com/baidubce/bce-sdk-go v0.9.217 h1:dbMeVzpr9BGItTFHB1s2KSrpz0ayJC1y366VUMmaF0k= +github.com/baidubce/bce-sdk-go v0.9.217/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index 6c89b794..65ddde65 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -10,6 +10,7 @@ import ( pAliyun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun" pAWSRoute53 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/aws-route53" pAzureDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/azure-dns" + pBaiduCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud" pCloudflare "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cloudflare" pClouDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cloudns" pGcore "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gcore" @@ -102,6 +103,22 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { return applicant, err } + case domain.ApplyDNSProviderTypeBaiduCloud, domain.ApplyDNSProviderTypeBaiduCloudDNS: + { + access := domain.AccessConfigForBaiduCloud{} + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + applicant, err := pBaiduCloud.NewChallengeProvider(&pBaiduCloud.BaiduCloudApplicantConfig{ + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + } + case domain.ApplyDNSProviderTypeCloudflare: { access := domain.AccessConfigForCloudflare{} diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 2aaa69a9..93f85561 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -67,6 +67,8 @@ const ( ApplyDNSProviderTypeAWS = ApplyDNSProviderType("aws") // 兼容旧值,等同于 [ApplyDNSProviderTypeAWSRoute53] ApplyDNSProviderTypeAWSRoute53 = ApplyDNSProviderType("aws-route53") ApplyDNSProviderTypeAzureDNS = ApplyDNSProviderType("azure-dns") + ApplyDNSProviderTypeBaiduCloud = ApplyDNSProviderType("baiducloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeBaiduCloudDNS] + ApplyDNSProviderTypeBaiduCloudDNS = ApplyDNSProviderType("baiducloud-dns") ApplyDNSProviderTypeCloudflare = ApplyDNSProviderType("cloudflare") ApplyDNSProviderTypeClouDNS = ApplyDNSProviderType("cloudns") ApplyDNSProviderTypeGcore = ApplyDNSProviderType("gcore") diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/baiducloud.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/baiducloud.go new file mode 100644 index 00000000..1328009a --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/baiducloud.go @@ -0,0 +1,39 @@ +package baiducloud + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + + internal "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/internal" +) + +type BaiduCloudApplicantConfig struct { + AccessKeyId string `json:"accessKeyId"` + SecretAccessKey string `json:"secretAccessKey"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *BaiduCloudApplicantConfig) (challenge.Provider, error) { + if config == nil { + panic("config is nil") + } + + providerConfig := internal.NewDefaultConfig() + providerConfig.AccessKeyID = config.AccessKeyId + providerConfig.SecretAccessKey = config.SecretAccessKey + 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/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/internal/lego.go new file mode 100644 index 00000000..fd138c6d --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/internal/lego.go @@ -0,0 +1,195 @@ +package lego_baiducloud + +import ( + "errors" + "fmt" + "strings" + "time" + + bceDns "github.com/baidubce/bce-sdk-go/services/dns" + "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/google/uuid" +) + +const ( + envNamespace = "BAIDUCLOUD_" + + 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 int32 + HTTPTimeout time.Duration +} + +type DNSProvider struct { + client *bceDns.Client + 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(EnvAccessKeyID, EnvSecretAccessKey) + if err != nil { + return nil, fmt.Errorf("baiducloud: %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("baiducloud: the configuration of the DNS provider is nil") + } + + client, err := bceDns.NewClient(config.AccessKeyID, config.SecretAccessKey, "") + if err != nil { + return nil, err + } else { + if client.Config != nil { + client.Config.ConnectionTimeoutInMillis = int(config.HTTPTimeout.Milliseconds()) + } + } + + return &DNSProvider{ + client: client, + config: config, + }, nil +} + +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + zoneName, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("baiducloud: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zoneName) + if err != nil { + return fmt.Errorf("baiducloud: %w", err) + } + + if err := d.addOrUpdateDNSRecord(domain, subDomain, info.Value); err != nil { + return fmt.Errorf("baiducloud: %w", err) + } + + return nil +} + +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + subDomain := dns01.UnFqdn(fqdn) + + if err := d.removeDNSRecord(domain, subDomain, value); err != nil { + return fmt.Errorf("baiducloud: %w", err) + } + + return nil +} + +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) getDNSRecord(domain, subDomain string) (*bceDns.Record, error) { + pageMarker := "" + pageSize := 1000 + for { + request := &bceDns.ListRecordRequest{} + request.Rr = domain + request.Marker = pageMarker + request.MaxKeys = pageSize + + response, err := d.client.ListRecord(domain, request) + if err != nil { + return nil, err + } + + for _, record := range response.Records { + if record.Type == "TXT" && record.Rr == subDomain { + return &record, nil + } + } + + if len(response.Records) < pageSize { + break + } + + pageMarker = response.NextMarker + } + + return nil, nil +} + +func (d *DNSProvider) addOrUpdateDNSRecord(domain, subDomain, value string) error { + record, err := d.getDNSRecord(domain, subDomain) + if err != nil { + return err + } + + if record == nil { + request := &bceDns.CreateRecordRequest{ + Type: "TXT", + Rr: subDomain, + Value: value, + Ttl: &d.config.TTL, + } + err := d.client.CreateRecord(domain, request, d.generateClientToken()) + return err + } else { + request := &bceDns.UpdateRecordRequest{ + Type: "TXT", + Rr: subDomain, + Value: value, + Ttl: &d.config.TTL, + } + err := d.client.UpdateRecord(domain, record.Id, request, d.generateClientToken()) + return err + } +} + +func (d *DNSProvider) removeDNSRecord(domain, subDomain, value string) error { + record, err := d.getDNSRecord(domain, subDomain) + if err != nil { + return err + } + + if record == nil { + return nil + } + + err = d.client.DeleteRecord(domain, record.Id, d.generateClientToken()) + return err +} + +func (d *DNSProvider) generateClientToken() string { + return strings.ReplaceAll(uuid.New().String(), "-", "") +} diff --git a/internal/pkg/utils/maps/maps.go b/internal/pkg/utils/maps/maps.go index a88b6629..4a4417d0 100644 --- a/internal/pkg/utils/maps/maps.go +++ b/internal/pkg/utils/maps/maps.go @@ -207,8 +207,3 @@ func Populate(dict map[string]any, output any) error { return decoder.Decode(dict) } - -// Deprecated: Use [Populate] instead. -func Decode(dict map[string]any, output any) error { - return Populate(dict, output) -} diff --git a/main.go b/main.go index 73d1a2a9..76f7f1c0 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,11 @@ func main() { var flagDir string flag.StringVar(&flagHttp, "http", "127.0.0.1:8090", "HTTP server address") flag.StringVar(&flagDir, "dir", "/pb_data/database", "Pocketbase data directory") + if len(os.Args) < 2 { + slog.Error("[CERTIMATE] missing exec args") + os.Exit(1) + return + } _ = flag.CommandLine.Parse(os.Args[2:]) // skip the first two arguments: "main.go serve" migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{ diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index ed14db23..b5bcaca7 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -67,11 +67,11 @@ export const accessProvidersMap: Map