diff --git a/go.mod b/go.mod index c658cfcaf..18ae5b6ff 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/acm v1.38.0 github.com/aws/aws-sdk-go-v2/service/cloudfront v1.61.0 github.com/aws/aws-sdk-go-v2/service/iam v1.53.7 + github.com/aws/aws-sdk-go-v2/service/lightsail v1.53.0 github.com/baidubce/bce-sdk-go v0.9.264 github.com/byteplus-sdk/byteplus-sdk-golang v1.0.63 github.com/go-acme/lego/v4 v4.34.0 diff --git a/go.sum b/go.sum index 919facc12..becee1c02 100644 --- a/go.sum +++ b/go.sum @@ -223,6 +223,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhL github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.53.0 h1:4A3tpD+CkJ77Yx9GEupVwMqmgscej35KjNtmnWWBewQ= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.53.0/go.mod h1:4nljpzVOSvMWeN9E7gMMa9aGWZVY0IVkhJ2DygRkV8c= github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5 h1:Z+/OLsb85Kpq7TVLCspskqePaf68Tdv6GfmJP4kH6i0= github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5/go.mod h1:TmxGowuBYwjmHFOsEDxaZdsQE62JJzOmtiWafTi/czg= github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg= diff --git a/internal/certacme/certifiers/sp_aws_lightsail.go b/internal/certacme/certifiers/sp_aws_lightsail.go new file mode 100644 index 000000000..37bba4340 --- /dev/null +++ b/internal/certacme/certifiers/sp_aws_lightsail.go @@ -0,0 +1,29 @@ +package certifiers + +import ( + "fmt" + + "github.com/go-acme/lego/v4/challenge" + + "github.com/certimate-go/certimate/internal/domain" + awslightsail "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/aws-lightsail" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeAWSLightsail, func(options *ProviderFactoryOptions) (challenge.Provider, error) { + credentials := domain.AccessConfigForAWS{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := awslightsail.NewChallenger(&awslightsail.ChallengerConfig{ + AccessKeyId: credentials.AccessKeyId, + SecretAccessKey: credentials.SecretAccessKey, + Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return provider, err + }) +} diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 637f06754..832313a4f 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -170,6 +170,7 @@ const ( ACMEDns01ProviderTypeAliyunESA = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-esa") ACMEDns01ProviderTypeArvanCloud = ACMEDns01ProviderType(AccessProviderTypeArvanCloud) ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53] + ACMEDns01ProviderTypeAWSLightsail = ACMEDns01ProviderType(AccessProviderTypeAWS + "-lightsail") ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53") ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure] ACMEDns01ProviderTypeAzureDNS = ACMEDns01ProviderType(AccessProviderTypeAzure + "-dns") diff --git a/pkg/core/certifier/challengers/dns01/aws-lightsail/aws_lightsail.go b/pkg/core/certifier/challengers/dns01/aws-lightsail/aws_lightsail.go new file mode 100644 index 000000000..1a3825915 --- /dev/null +++ b/pkg/core/certifier/challengers/dns01/aws-lightsail/aws_lightsail.go @@ -0,0 +1,42 @@ +package awslightsail + +import ( + "errors" + "time" + + internal "github.com/go-acme/lego/v4/providers/dns/route53" + + "github.com/certimate-go/certimate/pkg/core/certifier" +) + +type ChallengerConfig struct { + AccessKeyId string `json:"accessKeyId"` + SecretAccessKey string `json:"secretAccessKey"` + Region string `json:"region"` + DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int `json:"dnsTTL,omitempty"` +} + +func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { + if config == nil { + return nil, errors.New("the configuration of the acme challenge provider is nil") + } + + providerConfig := internal.NewDefaultConfig() + providerConfig.AccessKeyID = config.AccessKeyId + providerConfig.SecretAccessKey = config.SecretAccessKey + providerConfig.Region = config.Region + 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/aws-lightsail/internal/lego.go b/pkg/core/certifier/challengers/dns01/aws-lightsail/internal/lego.go new file mode 100644 index 000000000..6e5b24df6 --- /dev/null +++ b/pkg/core/certifier/challengers/dns01/aws-lightsail/internal/lego.go @@ -0,0 +1,150 @@ +package internal + +import ( + "context" + "errors" + "fmt" + "math/rand/v2" + "strconv" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/retry" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/lightsail" + awstypes "github.com/aws/aws-sdk-go-v2/service/lightsail/types" + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" +) + +const ( + envNamespace = "LIGHTSAIL_" + + EnvRegion = envNamespace + "REGION" + + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" +) + +const maxRetries = 5 + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +type Config struct { + AccessKeyID string + SecretAccessKey string + SessionToken string + Region string + + PropagationTimeout time.Duration + PollingInterval time.Duration +} + +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + } +} + +type DNSProvider struct { + client *lightsail.Client + config *Config +} + +func NewDNSProvider() (*DNSProvider, error) { + config := NewDefaultConfig() + config.Region = env.GetOrDefaultString(EnvRegion, "us-east-1") + + return NewDNSProviderConfig(config) +} + +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("lightsail: the configuration of the DNS provider is nil") + } + + ctx := context.Background() + cfg, err := awsconfig.LoadDefaultConfig(ctx, + awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(config.AccessKeyID, config.SecretAccessKey, config.SessionToken)), + awsconfig.WithRegion(config.Region), + awsconfig.WithRetryer(func() aws.Retryer { + return retry.NewStandard(func(options *retry.StandardOptions) { + options.MaxAttempts = maxRetries + options.Backoff = retry.BackoffDelayerFunc(func(attempt int, err error) (time.Duration, error) { + retryCount := min(attempt, 7) + delay := (1 << uint(retryCount)) * (rand.IntN(50) + 200) + return time.Duration(delay) * time.Millisecond, nil + }) + }) + }), + ) + if err != nil { + return nil, err + } + + return &DNSProvider{ + config: config, + client: lightsail.NewFromConfig(cfg), + }, nil +} + +func (d *DNSProvider) Present(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("lightsail: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("lightsail: %w", err) + } + + if _, err := d.client.CreateDomainEntry(context.Background(), &lightsail.CreateDomainEntryInput{ + DomainName: aws.String(dns01.UnFqdn(authZone)), + DomainEntry: &awstypes.DomainEntry{ + Type: aws.String("TXT"), + Name: aws.String(subDomain), + Target: aws.String(strconv.Quote(info.Value)), + }, + }); err != nil { + return fmt.Errorf("lightsail: %w", err) + } + + return nil +} + +func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("lightsail: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("lightsail: %w", err) + } + + if _, err := d.client.DeleteDomainEntry(context.Background(), &lightsail.DeleteDomainEntryInput{ + DomainName: aws.String(dns01.UnFqdn(authZone)), + DomainEntry: &awstypes.DomainEntry{ + Type: aws.String("TXT"), + Name: aws.String(subDomain), + Target: aws.String(strconv.Quote(info.Value)), + }, + }); err != nil { + return fmt.Errorf("lightsail: %w", err) + } + + return nil +} + +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/pkg/core/certifier/challengers/dns01/aws-route53/aws-route53.go b/pkg/core/certifier/challengers/dns01/aws-route53/aws_route53.go similarity index 100% rename from pkg/core/certifier/challengers/dns01/aws-route53/aws-route53.go rename to pkg/core/certifier/challengers/dns01/aws-route53/aws_route53.go diff --git a/pkg/core/certifier/challengers/dns01/azure-dns/azure-dns.go b/pkg/core/certifier/challengers/dns01/azure-dns/azure_dns.go similarity index 100% rename from pkg/core/certifier/challengers/dns01/azure-dns/azure-dns.go rename to pkg/core/certifier/challengers/dns01/azure-dns/azure_dns.go diff --git a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProvider.tsx index 2bd373e60..aba5cc695 100644 --- a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProvider.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { type ACMEDns01ProviderType, type ACMEHttp01ProviderType, ACME_DNS01_PROVIDERS, ACME_HTTP01_PROVIDERS } from "@/domain/provider"; import BizApplyNodeConfigFieldsProviderAliyunESA from "./BizApplyNodeConfigFieldsProviderAliyunESA"; +import BizApplyNodeConfigFieldsProviderAWSLightsail from "./BizApplyNodeConfigFieldsProviderAWSLightsail"; import BizApplyNodeConfigFieldsProviderAWSRoute53 from "./BizApplyNodeConfigFieldsProviderAWSRoute53"; import BizApplyNodeConfigFieldsProviderFTP from "./BizApplyNodeConfigFieldsProviderFTP"; import BizApplyNodeConfigFieldsProviderHuaweiCloudDNS from "./BizApplyNodeConfigFieldsProviderHuaweiCloudDNS"; @@ -18,6 +19,7 @@ const acmeDns01ProviderComponentMap: Partial { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + region: "us-east-1", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t } = i18n; + + return z.object({ + region: z.string().nonempty(t("workflow_node.apply.form.aws_lightsail_region.placeholder")), + }); +}; + +const _default = Object.assign(BizApplyNodeConfigFieldsProviderAWSLightsail, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index eafdf4d94..a72fee84e 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -359,6 +359,7 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({ ALIYUN_ESA: `${ACCESS_PROVIDERS.ALIYUN}-esa`, ARVANCLOUD: `${ACCESS_PROVIDERS.ARVANCLOUD}`, AWS: `${ACCESS_PROVIDERS.AWS}`, // 兼容旧值,等同于 `AWS_ROUTE53` + AWS_LIGHTSAIL: `${ACCESS_PROVIDERS.AWS}-lightsail`, AWS_ROUTE53: `${ACCESS_PROVIDERS.AWS}-route53`, AZURE: `${ACCESS_PROVIDERS.AZURE}`, // 兼容旧值,等同于 `AZURE_DNS` AZURE_DNS: `${ACCESS_PROVIDERS.AZURE}-dns`, @@ -445,6 +446,7 @@ export const acmeDns01ProvidersMap: Maphttps://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint", + "workflow_node.apply.form.aws_lightsail_region.label": "AWS Region", + "workflow_node.apply.form.aws_lightsail_region.placeholder": "Please enter AWS Lightsail region (e.g. us-east-1)", + "workflow_node.apply.form.aws_lightsail_region.tooltip": "For more information, see https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints", "workflow_node.apply.form.aws_route53_region.label": "AWS Region", "workflow_node.apply.form.aws_route53_region.placeholder": "Please enter AWS Route53 region (e.g. us-east-1)", "workflow_node.apply.form.aws_route53_region.tooltip": "For more information, see https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index ad833d83c..6f4851ab3 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -35,6 +35,7 @@ "provider.aws_acm": "AWS - ACM (Amazon Certificate Manager)", "provider.aws_cloudfront": "AWS - CloudFront", "provider.aws_iam": "AWS - IAM (Identity and Access Management)", + "provider.aws_lightsail": "AWS - Lightsail", "provider.aws_route53": "AWS - Route53", "provider.azure": "Azure", "provider.azure_dns": "Azure - DNS", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 600816bfd..2bb8b3d92 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -70,6 +70,9 @@ "workflow_node.apply.form.aliyun_esa_region.label": "阿里云服务地域", "workflow_node.apply.form.aliyun_esa_region.placeholder": "请输入阿里云 ESA 服务地域(例如:cn-hangzhou)", "workflow_node.apply.form.aliyun_esa_region.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint", + "workflow_node.apply.form.aws_lightsail_region.label": "AWS 服务区域", + "workflow_node.apply.form.aws_lightsail_region.placeholder": "请输入 AWS Lightsail 服务区域(例如:us-east-1)", + "workflow_node.apply.form.aws_lightsail_region.tooltip": "这是什么?请参阅 https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints", "workflow_node.apply.form.aws_route53_region.label": "AWS 服务区域", "workflow_node.apply.form.aws_route53_region.placeholder": "请输入 AWS Route53 服务区域(例如:us-east-1)", "workflow_node.apply.form.aws_route53_region.tooltip": "这是什么?请参阅 https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints",