From fa8ba061fb66e075e20a321b360aeb6e662e5437 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 20 Jan 2025 02:27:59 +0800 Subject: [PATCH] feat: support ARI --- internal/applicant/applicant.go | 77 ++++++++++++------- internal/domain/workflow.go | 2 + .../workflow/node-processor/apply_node.go | 6 +- .../workflow/node/ApplyNodeConfigForm.tsx | 16 +++- .../components/workflow/node/DeployNode.tsx | 5 +- .../workflow/node/DeployNodeConfigForm.tsx | 42 +++++----- ui/src/domain/workflow.ts | 1 + .../i18n/locales/en/nls.workflow.nodes.json | 6 +- .../i18n/locales/zh/nls.workflow.nodes.json | 8 +- 9 files changed, 100 insertions(+), 63 deletions(-) diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index b4536921..abee203a 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -14,10 +14,11 @@ import ( "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/lego" + "golang.org/x/exp/slices" "golang.org/x/time/rate" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/utils/slices" + uslices "github.com/usual2970/certimate/internal/pkg/utils/slices" "github.com/usual2970/certimate/internal/repository" ) @@ -45,7 +46,9 @@ type applicantOptions struct { DnsPropagationTimeout int32 DnsTTL int32 DisableFollowCNAME bool + DisableARI bool SkipBeforeExpiryDays int32 + ReplacedARICertId string } func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { @@ -54,32 +57,49 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { } nodeConfig := node.GetConfigForApply() - - accessRepo := repository.NewAccessRepository() - access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId) - if err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) - } - - accessConfig, err := access.UnmarshalConfigToMap() - if err != nil { - return nil, fmt.Errorf("failed to unmarshal access config: %w", err) - } - options := &applicantOptions{ - Domains: slices.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }), + Domains: uslices.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }), ContactEmail: nodeConfig.ContactEmail, Provider: domain.ApplyDNSProviderType(nodeConfig.Provider), - ProviderAccessConfig: accessConfig, ProviderApplyConfig: nodeConfig.ProviderConfig, KeyAlgorithm: nodeConfig.KeyAlgorithm, - Nameservers: slices.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }), + Nameservers: uslices.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }), DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout, DnsTTL: nodeConfig.DnsTTL, DisableFollowCNAME: nodeConfig.DisableFollowCNAME, + DisableARI: nodeConfig.DisableARI, SkipBeforeExpiryDays: nodeConfig.SkipBeforeExpiryDays, } + accessRepo := repository.NewAccessRepository() + if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + } else { + accessConfig, err := access.UnmarshalConfigToMap() + if err != nil { + return nil, fmt.Errorf("failed to unmarshal access config: %w", err) + } + + options.ProviderAccessConfig = accessConfig + } + + certRepo := repository.NewCertificateRepository() + lastCertificate, _ := certRepo.GetByWorkflowNodeId(context.Background(), node.Id) + if lastCertificate != nil { + newCertSan := slices.Clone(options.Domains) + oldCertSan := strings.Split(lastCertificate.SubjectAltNames, ";") + slices.Sort(newCertSan) + slices.Sort(oldCertSan) + + if slices.Equal(newCertSan, oldCertSan) { + lastCertX509, _ := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate)) + if lastCertX509 != nil { + replacedARICertId, _ := certificate.MakeARICertID(lastCertX509) + options.ReplacedARICertId = replacedARICertId + } + } + } + applicant, err := createApplicant(options) if err != nil { return nil, err @@ -109,7 +129,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap sslProviderConfig.Provider = defaultSSLProvider } - myUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail) + acmeUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail) if err != nil { return nil, err } @@ -118,39 +138,42 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap // link: https://github.com/go-acme/lego/issues/1867 os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(options.DisableFollowCNAME)) - config := lego.NewConfig(myUser) - - // This CA URL is configured for a local dev instance of Boulder running in Docker in a VM. + // Create an ACME client config + config := lego.NewConfig(acmeUser) config.CADirURL = sslProviderUrls[sslProviderConfig.Provider] config.Certificate.KeyType = parseKeyAlgorithm(options.KeyAlgorithm) - // A client facilitates communication with the CA server. + // Create an ACME client client, err := lego.NewClient(config) if err != nil { return nil, err } + // Set the DNS01 challenge provider challengeOptions := make([]dns01.ChallengeOption, 0) if len(options.Nameservers) > 0 { challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(dns01.ParseNameservers(options.Nameservers))) challengeOptions = append(challengeOptions, dns01.DisableAuthoritativeNssPropagationRequirement()) } - client.Challenge.SetDNS01Provider(challengeProvider, challengeOptions...) - // New users will need to register - if !myUser.hasRegistration() { - reg, err := registerAcmeUser(client, sslProviderConfig, myUser) + // New users need to register first + if !acmeUser.hasRegistration() { + reg, err := registerAcmeUser(client, sslProviderConfig, acmeUser) if err != nil { return nil, fmt.Errorf("failed to register: %w", err) } - myUser.Registration = reg + acmeUser.Registration = reg } + // Obtain a certificate certRequest := certificate.ObtainRequest{ Domains: options.Domains, Bundle: true, } + if !options.DisableARI { + certRequest.ReplacesCertID = options.ReplacedARICertId + } certResource, err := client.Certificate.Obtain(certRequest) if err != nil { return nil, err @@ -162,7 +185,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)), ACMECertUrl: certResource.CertURL, ACMECertStableUrl: certResource.CertStableURL, - CSR: string(certResource.CSR), + CSR: strings.TrimSpace(string(certResource.CSR)), }, nil } diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 51f446eb..6e957d72 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -68,6 +68,7 @@ type WorkflowNodeConfigForApply struct { DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(默认取决于提供商) DnsTTL int32 `json:"dnsTTL"` // DNS TTL(默认取决于提供商) DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否禁用 CNAME 跟随 + DisableARI bool `json:"disableARI"` // 是否禁用 ARI SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(默认值:30) } @@ -124,6 +125,7 @@ func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { DnsPropagationTimeout: n.getConfigValueAsInt32("dnsPropagationTimeout"), DnsTTL: n.getConfigValueAsInt32("dnsTTL"), DisableFollowCNAME: n.getConfigValueAsBool("disableFollowCNAME"), + DisableARI: n.getConfigValueAsBool("disableARI"), SkipBeforeExpiryDays: skipBeforeExpiryDays, } } diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 28a7588a..11725d53 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -2,6 +2,7 @@ package nodeprocessor import ( "context" + "fmt" "strings" "time" @@ -131,8 +132,9 @@ func (a *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo lastCertificate, _ := a.certRepo.GetByWorkflowNodeId(ctx, a.node.Id) renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 - if lastCertificate != nil && time.Until(lastCertificate.ExpireAt) > renewalInterval { - return true, "已申请过证书,且证书尚未临近过期" + expirationTime := time.Until(lastCertificate.ExpireAt) + if lastCertificate != nil && expirationTime > renewalInterval { + return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(到期尚余 %d 天,距 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) } } diff --git a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx index 5a3fb9f1..2280fbb8 100644 --- a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx @@ -57,7 +57,6 @@ const MULTIPLE_INPUT_DELIMITER = ";"; const initFormModel = (): ApplyNodeConfigFormFieldValues => { return { keyAlgorithm: "RSA2048", - disableFollowCNAME: true, skipBeforeExpiryDays: 20, }; }; @@ -105,11 +104,11 @@ const ApplyNodeConfigForm = forwardRef + + } + > + + @@ -397,7 +405,7 @@ const ApplyNodeConfigForm = forwardRef diff --git a/ui/src/components/workflow/node/DeployNode.tsx b/ui/src/components/workflow/node/DeployNode.tsx index 62a81470..db5830da 100644 --- a/ui/src/components/workflow/node/DeployNode.tsx +++ b/ui/src/components/workflow/node/DeployNode.tsx @@ -92,11 +92,8 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => { pending={formPending} onConfirm={handleDrawerConfirm} onOpenChange={(open) => { + setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider); setDrawerOpen(open); - - if (!open) { - setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider); - } }} getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 9a9e22d2..542504e3 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -342,28 +342,28 @@ const DeployNodeConfigForm = forwardRef {nestedFormEl} + + + + {t("workflow_node.deploy.form.strategy_config.label")} + + + +
+ + +
{t("workflow_node.deploy.form.skip_on_last_succeeded.prefix")}
+ + + +
{t("workflow_node.deploy.form.skip_on_last_succeeded.suffix")}
+
+
+
- - - - {t("workflow_node.deploy.form.strategy_config.label")} - - - -
- - -
{t("workflow_node.deploy.form.skip_on_last_succeeded.prefix")}
- - - -
{t("workflow_node.deploy.form.skip_on_last_succeeded.suffix")}
-
-
-
); } diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index b984e525..d611cd4f 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -111,6 +111,7 @@ export type WorkflowNodeConfigForApply = { dnsPropagationTimeout?: number; dnsTTL?: number; disableFollowCNAME?: boolean; + disableARI?: boolean; skipBeforeExpiryDays: number; }; diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 89ab8820..efcf2247 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -51,7 +51,7 @@ "workflow_node.apply.form.key_algorithm.placeholder": "Please select certificate key algorithm", "workflow_node.apply.form.nameservers.label": "DNS recursive nameservers (Optional)", "workflow_node.apply.form.nameservers.placeholder": "Please enter DNS recursive nameservers (separated by semicolons)", - "workflow_node.apply.form.nameservers.tooltip": "It determines whether to custom DNS recursive nameservers during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.Learn more.", + "workflow_node.apply.form.nameservers.tooltip": "It determines whether to custom DNS recursive nameservers during ACME DNS-01 authentication. If you don't understand this option, just keep it by default. Learn more.", "workflow_node.apply.form.nameservers.multiple_input_modal.title": "Change DNS rcursive nameservers", "workflow_node.apply.form.nameservers.multiple_input_modal.placeholder": "Please enter DNS recursive nameserver", "workflow_node.apply.form.dns_propagation_timeout.label": "DNS propagation timeout (Optional)", @@ -63,7 +63,9 @@ "workflow_node.apply.form.dns_ttl.unit": "seconds", "workflow_node.apply.form.dns_ttl.tooltip": "It determines the time to live for DNS record during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.

Leave blank to use the default value provided by the provider.", "workflow_node.apply.form.disable_follow_cname.label": "Disable CNAME following", - "workflow_node.apply.form.disable_follow_cname.tooltip": "It determines whether to disable CNAME following during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.Learn more.", + "workflow_node.apply.form.disable_follow_cname.tooltip": "It determines whether to disable CNAME following during ACME DNS-01 authentication. If you don't understand this option, just keep it by default. Learn more.", + "workflow_node.apply.form.disable_ari.label": "Disable ARI", + "workflow_node.apply.form.disable_ari.tooltip": "It determines whether to disable ARI (ACME Renewal Information). If you don't understand this option, just keep it by default. Learn more.", "workflow_node.apply.form.strategy_config.label": "Strategy settings", "workflow_node.apply.form.skip_before_expiry_days.label": "Renewal interval", "workflow_node.apply.form.skip_before_expiry_days.placeholder": "Please enter renewal interval", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 51114fc2..ff1732cc 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -62,12 +62,14 @@ "workflow_node.apply.form.dns_ttl.placeholder": "请输入 DNS 解析 TTL", "workflow_node.apply.form.dns_ttl.unit": "秒", "workflow_node.apply.form.dns_ttl.tooltip": "在 ACME DNS-01 认证时 DNS 解析记录的 TTL。如果你不了解此选项的用途,保持默认即可。

为空时,将使用提供商提供的默认值。", - "workflow_node.apply.form.disable_follow_cname.label": "禁止 CNAME 跟随", - "workflow_node.apply.form.disable_follow_cname.tooltip": "在 ACME DNS-01 认证时是否禁止 CNAME 跟随。如果你不了解该选项的用途,保持默认即可。点此了解更多。", + "workflow_node.apply.form.disable_follow_cname.label": "关闭 CNAME 跟随", + "workflow_node.apply.form.disable_follow_cname.tooltip": "在 ACME DNS-01 认证时是否关闭 CNAME 跟随。如果你不了解该选项的用途,保持默认即可。点此了解更多。", + "workflow_node.apply.form.disable_ari.label": "关闭 ARI 续期", + "workflow_node.apply.form.disable_ari.tooltip": "在 ACME 证书续期时是否关闭 ARI(ACME Renewal Information)。如果你不了解该选项的用途,保持默认即可。点此了解更多。", "workflow_node.apply.form.strategy_config.label": "执行策略", "workflow_node.apply.form.skip_before_expiry_days.label": "续期间隔", "workflow_node.apply.form.skip_before_expiry_days.placeholder": "请输入续期间隔", - "workflow_node.apply.form.skip_before_expiry_days.prefix": "当上次签发的证书到期时间超过", + "workflow_node.apply.form.skip_before_expiry_days.prefix": "当上次签发的证书距到期时间超过", "workflow_node.apply.form.skip_before_expiry_days.suffix": "时,跳过重新申请。", "workflow_node.apply.form.skip_before_expiry_days.unit": "天", "workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过 CA 的证书有效期限制,否则证书可能永远不会续期。",