diff --git a/internal/certdeploy/deployers/sp_aliyun_ddospro.go b/internal/certdeploy/deployers/sp_aliyun_ddospro.go index 906cafec..7271794e 100644 --- a/internal/certdeploy/deployers/sp_aliyun_ddospro.go +++ b/internal/certdeploy/deployers/sp_aliyun_ddospro.go @@ -17,11 +17,12 @@ func init() { } provider, err := aliyunddospro.NewSSLDeployerProvider(&aliyunddospro.SSLDeployerProviderConfig{ - AccessKeyId: credentials.AccessKeyId, - AccessKeySecret: credentials.AccessKeySecret, - ResourceGroupId: credentials.ResourceGroupId, - Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), - Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), + AccessKeyId: credentials.AccessKeyId, + AccessKeySecret: credentials.AccessKeySecret, + ResourceGroupId: credentials.ResourceGroupId, + Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), + DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), + Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) diff --git a/pkg/core/ssl-deployer/providers/aliyun-ddospro/aliyun_ddospro.go b/pkg/core/ssl-deployer/providers/aliyun-ddospro/aliyun_ddospro.go index b69cabe5..1984de8d 100644 --- a/pkg/core/ssl-deployer/providers/aliyun-ddospro/aliyun_ddospro.go +++ b/pkg/core/ssl-deployer/providers/aliyun-ddospro/aliyun_ddospro.go @@ -16,6 +16,8 @@ import ( "github.com/certimate-go/certimate/pkg/core" "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/aliyun-ddospro/internal" sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/aliyun-cas" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type SSLDeployerProviderConfig struct { @@ -27,6 +29,9 @@ type SSLDeployerProviderConfig struct { ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` + // 域名匹配模式。 + // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 + DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 网站域名(支持泛域名)。 Domain string `json:"domain"` } @@ -81,10 +86,6 @@ func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) { } func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) { - if d.config.Domain == "" { - return nil, errors.New("config `domain` is required") - } - // 上传证书 upres, err := d.sslManager.Upload(ctx, certPEM, privkeyPEM) if err != nil { @@ -93,19 +94,127 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } + // 获取待部署的域名列表 + var domains []string + switch d.config.DomainMatchPattern { + case "", DOMAIN_MATCH_PATTERN_EXACT: + { + if d.config.Domain == "" { + return nil, errors.New("config `domain` is required") + } + + domains = []string{d.config.Domain} + } + + case DOMAIN_MATCH_PATTERN_WILDCARD: + { + if d.config.Domain == "" { + return nil, errors.New("config `domain` is required") + } + + if strings.HasPrefix(d.config.Domain, "*.") { + domainCandidates, err := d.getAllDomains(ctx) + if err != nil { + return nil, err + } + + domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { + return xcerthostname.IsMatch(d.config.Domain, domain) + }) + if len(domains) == 0 { + return nil, errors.New("could not find any domains matched by wildcard") + } + } else { + domains = []string{d.config.Domain} + } + } + + case DOMAIN_MATCH_PATTERN_CERTSAN: + { + certX509, err := xcert.ParseCertificateFromPEM(certPEM) + if err != nil { + return nil, err + } + + domainCandidates, err := d.getAllDomains(ctx) + if err != nil { + return nil, err + } + + domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { + return certX509.VerifyHostname(domain) == nil + }) + if len(domains) == 0 { + return nil, errors.New("could not find any domains matched by certificate") + } + } + + default: + return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) + } + + // 遍历更新域名证书 + if len(domains) == 0 { + d.logger.Info("no ddoscoo domains to deploy") + } else { + d.logger.Info("found ddoscoo domains to deploy", slog.Any("domains", domains)) + var errs []error + + for _, domain := range domains { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + certId := upres.ExtendedData["CertIdentifier"].(string) + if err := d.updateDomainCertificate(ctx, domain, certId); err != nil { + errs = append(errs, err) + } + } + } + + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + } + + return &core.SSLDeployResult{}, nil +} + +func (d *SSLDeployerProvider) getAllDomains(ctx context.Context) ([]string, error) { + domains := make([]string, 0) + + // 查询已配置网站业务转发规则的域名 + // REF: https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-describedomains + describeDomainsReq := &aliddoscoo.DescribeDomainsRequest{ + ResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), + } + describeDomainsResp, err := d.sdkClient.DescribeDomainsWithContext(ctx, describeDomainsReq, &dara.RuntimeOptions{}) + d.logger.Debug("sdk request 'aliddoscoo.DescribeLiveUserDomains'", slog.Any("request", describeDomainsReq), slog.Any("response", describeDomainsResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'aliddoscoo.DescribeDomains': %w", err) + } + + for _, domain := range describeDomainsResp.Body.Domains { + domains = append(domains, tea.StringValue(domain)) + } + + return domains, nil +} + +func (d *SSLDeployerProvider) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 为网站业务转发规则关联 SSL 证书 // REF: https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-associatewebcert associateWebCertReq := &aliddoscoo.AssociateWebCertRequest{ - Domain: tea.String(d.config.Domain), - CertIdentifier: tea.String(upres.ExtendedData["CertIdentifier"].(string)), + Domain: tea.String(domain), + CertIdentifier: tea.String(cloudCertId), } associateWebCertResp, err := d.sdkClient.AssociateWebCertWithContext(ctx, associateWebCertReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'dcdn.AssociateWebCert'", slog.Any("request", associateWebCertReq), slog.Any("response", associateWebCertResp)) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'dcdn.AssociateWebCert': %w", err) + return fmt.Errorf("failed to execute sdk request 'dcdn.AssociateWebCert': %w", err) } - return &core.SSLDeployResult{}, nil + return nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.DdoscooClient, error) { diff --git a/pkg/core/ssl-deployer/providers/aliyun-ddospro/aliyun_ddospro_test.go b/pkg/core/ssl-deployer/providers/aliyun-ddospro/aliyun_ddospro_test.go index 458e1928..aec9989a 100644 --- a/pkg/core/ssl-deployer/providers/aliyun-ddospro/aliyun_ddospro_test.go +++ b/pkg/core/ssl-deployer/providers/aliyun-ddospro/aliyun_ddospro_test.go @@ -57,10 +57,11 @@ func TestDeploy(t *testing.T) { }, "\n")) deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{ - AccessKeyId: fAccessKeyId, - AccessKeySecret: fAccessKeySecret, - Region: fRegion, - Domain: fDomain, + AccessKeyId: fAccessKeyId, + AccessKeySecret: fAccessKeySecret, + Region: fRegion, + DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, + Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) diff --git a/pkg/core/ssl-deployer/providers/aliyun-ddospro/consts.go b/pkg/core/ssl-deployer/providers/aliyun-ddospro/consts.go new file mode 100644 index 00000000..2c25f12d --- /dev/null +++ b/pkg/core/ssl-deployer/providers/aliyun-ddospro/consts.go @@ -0,0 +1,10 @@ +package aliyunddospro + +const ( + // 匹配模式:精确匹配。 + DOMAIN_MATCH_PATTERN_EXACT = "exact" + // 匹配模式:通配符匹配。 + DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" + // 匹配模式:证书 SAN 匹配。 + DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" +) diff --git a/pkg/core/ssl-deployer/providers/aliyun-ddospro/internal/client.go b/pkg/core/ssl-deployer/providers/aliyun-ddospro/internal/client.go index 837d3507..989e0831 100644 --- a/pkg/core/ssl-deployer/providers/aliyun-ddospro/internal/client.go +++ b/pkg/core/ssl-deployer/providers/aliyun-ddospro/internal/client.go @@ -92,3 +92,41 @@ func (client *DdoscooClient) AssociateWebCertWithContext(ctx context.Context, re _err = dara.Convert(_body, &_result) return _result, _err } + +func (client *DdoscooClient) DescribeDomainsWithContext(ctx context.Context, request *aliddoscoo.DescribeDomainsRequest, runtime *dara.RuntimeOptions) (_result *aliddoscoo.DescribeDomainsResponse, _err error) { + _err = request.Validate() + if _err != nil { + return _result, _err + } + query := map[string]interface{}{} + + if !dara.IsNil(request.InstanceIds) { + query["InstanceIds"] = request.InstanceIds + } + + if !dara.IsNil(request.ResourceGroupId) { + query["ResourceGroupId"] = request.ResourceGroupId + } + + req := &openapiutil.OpenApiRequest{ + Query: openapiutil.Query(query), + } + params := &openapiutil.Params{ + Action: dara.String("DescribeDomains"), + Version: dara.String("2020-01-01"), + Protocol: dara.String("HTTPS"), + Pathname: dara.String("/"), + Method: dara.String("POST"), + AuthType: dara.String("AK"), + Style: dara.String("RPC"), + ReqBodyType: dara.String("formData"), + BodyType: dara.String("json"), + } + _result = &aliddoscoo.DescribeDomainsResponse{} + _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) + if _err != nil { + return _result, _err + } + _err = dara.Convert(_body, &_result) + return _result, _err +} diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDDoSPro.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDDoSPro.tsx index 5313a8d2..4f1fff9b 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDDoSPro.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDDoSPro.tsx @@ -1,12 +1,17 @@ import { getI18n, useTranslation } from "react-i18next"; -import { Form, Input } from "antd"; +import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import Show from "@/components/Show"; import { validDomainName } from "@/utils/validators"; import { useFormNestedFieldsContext } from "./_context"; +const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; +const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; +const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; + const BizDeployNodeConfigFieldsProviderAliyunDDoSPro = () => { const { i18n, t } = useTranslation(); @@ -15,8 +20,11 @@ const BizDeployNodeConfigFieldsProviderAliyunDDoSPro = () => { [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); + const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); + const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); + return ( <>