From 186a18db6d3d545eccc73fef9a14194982f36dbe Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 3 Nov 2025 13:38:09 +0800 Subject: [PATCH] feat: reusing certificates key --- cmd/intercmd.go | 2 +- internal/certapply/client_certifier.go | 19 +- internal/certificate/service.go | 18 +- internal/domain/certificate.go | 58 +---- internal/domain/dtos/certificate.go | 3 +- internal/domain/workflow.go | 10 +- internal/workflow/engine/executor_bizapply.go | 56 +++-- migrations/1760486400_m0.4.1.go | 99 ++++---- migrations/1761883200_m0.4.3.go | 56 ----- migrations/1762142400_m0.4.3.go | 218 ++++++++++++++++++ .../tencentcloud-cdn/tencentcloud_cdn.go | 4 +- .../tencentcloud-ecdn/tencentcloud_ecdn.go | 4 +- .../tencentcloud-eo/tencentcloud_eo.go | 4 +- .../volcengine-cdn/volcengine_cdn.go | 4 +- .../volcengine-live/volcengine_live.go | 4 +- pkg/utils/cert/{ => hostname}/hostname.go | 4 +- .../cert/{ => hostname}/hostname_test.go | 10 +- pkg/utils/crypto/key/key.go | 46 ++++ ui/src/api/certificates.ts | 34 +-- ui/src/api/workflows.ts | 13 +- .../workflow/designer/elements/DragNode.tsx | 4 +- .../designer/forms/BizApplyNodeConfigForm.tsx | 109 ++++++++- .../forms/BizDeployNodeConfigForm.tsx | 2 +- .../forms/BizNotifyNodeConfigForm.tsx | 2 +- .../forms/BizUploadNodeConfigForm.tsx | 5 +- .../designer/forms/DelayNodeConfigForm.tsx | 2 +- .../designer/forms/StartNodeConfigForm.tsx | 2 +- .../workflow/designer/forms/_shared.tsx | 12 +- ui/src/domain/workflow.ts | 3 + .../i18n/locales/en/nls.workflow.nodes.json | 15 +- ui/src/i18n/locales/zh/nls.certificate.json | 2 +- ui/src/i18n/locales/zh/nls.settings.json | 2 +- .../i18n/locales/zh/nls.workflow.nodes.json | 15 +- 33 files changed, 586 insertions(+), 255 deletions(-) delete mode 100644 migrations/1761883200_m0.4.3.go create mode 100644 migrations/1762142400_m0.4.3.go rename pkg/utils/cert/{ => hostname}/hostname.go (90%) rename pkg/utils/cert/{ => hostname}/hostname_test.go (81%) create mode 100644 pkg/utils/crypto/key/key.go diff --git a/cmd/intercmd.go b/cmd/intercmd.go index 49268217..235649cb 100644 --- a/cmd/intercmd.go +++ b/cmd/intercmd.go @@ -62,7 +62,7 @@ func internalCertApplyCommand(app core.App) *cobra.Command { client, err := certapply.NewACMEClientWithAccount(params.Account, func(c *lego.Config) error { c.UserAgent = "certimate" - c.Certificate.KeyType = params.Request.KeyType + c.Certificate.KeyType = params.Request.PrivateKeyType return nil }) if err != nil { diff --git a/internal/certapply/client_certifier.go b/internal/certapply/client_certifier.go index 982fcbf5..28aeab9f 100644 --- a/internal/certapply/client_certifier.go +++ b/internal/certapply/client_certifier.go @@ -2,6 +2,7 @@ import ( "context" + "crypto" "errors" "fmt" "os" @@ -22,9 +23,10 @@ import ( ) type ObtainCertificateRequest struct { - Domains []string - KeyType certcrypto.KeyType - ValidityTo time.Time + Domains []string + PrivateKeyType certcrypto.KeyType + PrivateKeyPEM string + ValidityTo time.Time // 提供商相关 ChallengeType string @@ -150,8 +152,19 @@ func (c *ACMEClient) sendObtainCertificateRequest(request *ObtainCertificateRequ return nil, fmt.Errorf("unsupported challenge type: '%s'", request.ChallengeType) } + var privkey crypto.PrivateKey + if request.PrivateKeyPEM != "" { + pk, err := certcrypto.ParsePEMPrivateKey([]byte(request.PrivateKeyPEM)) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + + privkey = pk + } + req := certificate.ObtainRequest{ Domains: request.Domains, + PrivateKey: privkey, Bundle: true, Profile: request.ACMEProfile, NotAfter: request.ValidityTo, diff --git a/internal/certificate/service.go b/internal/certificate/service.go index 4b80f482..bdd14080 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bytes" "context" + "crypto/x509" "fmt" "log/slog" "strings" @@ -16,6 +17,7 @@ import ( "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/domain/dtos" xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xcryptokey "github.com/certimate-go/certimate/pkg/utils/crypto/key" ) type CertificateService struct { @@ -230,13 +232,25 @@ func (s *CertificateService) ValidateCertificate(ctx context.Context, req *dtos. } func (s *CertificateService) ValidatePrivateKey(ctx context.Context, req *dtos.CertificateValidatePrivateKeyReq) (*dtos.CertificateValidatePrivateKeyResp, error) { - _, err := xcert.ParsePrivateKeyFromPEM(req.PrivateKey) + privkey, err := xcert.ParsePrivateKeyFromPEM(req.PrivateKey) if err != nil { return nil, err } + var keyAlgorithmString string + keyAlgorithm, keySize, _ := xcryptokey.GetPrivateKeyAlgorithm(privkey) + switch keyAlgorithm { + case x509.RSA: + keyAlgorithmString = fmt.Sprintf("RSA%d", keySize) + case x509.ECDSA: + keyAlgorithmString = fmt.Sprintf("EC%d", keySize) + case x509.Ed25519: + keyAlgorithmString = "ED25519" + } + return &dtos.CertificateValidatePrivateKeyResp{ - IsValid: true, + IsValid: keyAlgorithmString != "", + KeyAlgorithm: keyAlgorithmString, }, nil } diff --git a/internal/domain/certificate.go b/internal/domain/certificate.go index 171e78c4..d5afd9e3 100644 --- a/internal/domain/certificate.go +++ b/internal/domain/certificate.go @@ -1,8 +1,6 @@ package domain import ( - "crypto/ecdsa" - "crypto/rsa" "crypto/x509" "fmt" "strings" @@ -11,6 +9,7 @@ import ( "github.com/go-acme/lego/v4/certcrypto" xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xcryptokey "github.com/certimate-go/certimate/pkg/utils/crypto/key" ) const CollectionNameCertificate = "certificate" @@ -45,58 +44,14 @@ func (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate c.ValidityNotBefore = certX509.NotBefore c.ValidityNotAfter = certX509.NotAfter - switch certX509.PublicKeyAlgorithm { + keyAlgorithm, keySize, _ := xcryptokey.GetPublicKeyAlgorithm(certX509.PublicKey) + switch keyAlgorithm { case x509.RSA: - { - len := 0 - if pubkey, ok := certX509.PublicKey.(*rsa.PublicKey); ok { - len = pubkey.N.BitLen() - } - - switch len { - case 0: - c.KeyAlgorithm = CertificateKeyAlgorithmType("RSA") - case 2048: - c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA2048 - case 3072: - c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA3072 - case 4096: - c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA4096 - case 8192: - c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA8192 - default: - c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("RSA%d", len)) - } - } - + c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("RSA%d", keySize)) case x509.ECDSA: - { - len := 0 - if pubkey, ok := certX509.PublicKey.(*ecdsa.PublicKey); ok { - if pubkey.Curve != nil && pubkey.Curve.Params() != nil { - len = pubkey.Curve.Params().BitSize - } - } - - switch len { - case 0: - c.KeyAlgorithm = CertificateKeyAlgorithmType("EC") - case 256: - c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC256 - case 384: - c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC384 - case 521: - c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC512 - default: - c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("EC%d", len)) - } - } - + c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("EC%d", keySize)) case x509.Ed25519: - { - c.KeyAlgorithm = CertificateKeyAlgorithmType("ED25519") - } - + c.KeyAlgorithm = CertificateKeyAlgorithmType("ED25519") default: c.KeyAlgorithm = CertificateKeyAlgorithmType("") } @@ -146,7 +101,6 @@ func (t CertificateKeyAlgorithmType) KeyType() (certcrypto.KeyType, error) { CertificateKeyAlgorithmTypeRSA8192: certcrypto.RSA8192, CertificateKeyAlgorithmTypeEC256: certcrypto.EC256, CertificateKeyAlgorithmTypeEC384: certcrypto.EC384, - CertificateKeyAlgorithmTypeEC512: certcrypto.KeyType("P512"), } if keyType, ok := keyTypeMap[t]; ok { diff --git a/internal/domain/dtos/certificate.go b/internal/domain/dtos/certificate.go index 5559bfc9..6b721f6c 100644 --- a/internal/domain/dtos/certificate.go +++ b/internal/domain/dtos/certificate.go @@ -30,5 +30,6 @@ type CertificateValidatePrivateKeyReq struct { } type CertificateValidatePrivateKeyResp struct { - IsValid bool `json:"isValid"` + IsValid bool `json:"isValid"` + KeyAlgorithm string `json:"keyAlgorithm,omitempty"` } diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 5d13bcec..dd81e16a 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -146,7 +146,9 @@ func (c WorkflowNodeConfig) AsBizApply() WorkflowNodeConfigForBizApply { Provider: xmaps.GetString(c, "provider"), ProviderAccessId: xmaps.GetString(c, "providerAccessId"), ProviderConfig: xmaps.GetKVMapAny(c, "providerConfig"), + KeySource: xmaps.GetOrDefaultString(c, "keySource", "auto"), KeyAlgorithm: xmaps.GetOrDefaultString(c, "keyAlgorithm", string(CertificateKeyAlgorithmTypeRSA2048)), + KeyContent: xmaps.GetString(c, "keyContent"), CAProvider: xmaps.GetString(c, "caProvider"), CAProviderAccessId: xmaps.GetString(c, "caProviderAccessId"), CAProviderConfig: xmaps.GetKVMapAny(c, "caProviderConfig"), @@ -220,8 +222,10 @@ type WorkflowNodeConfigForBizApply struct { CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值时使用全局配置) CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置 - KeyAlgorithm string `json:"keyAlgorithm,omitempty"` // 证书算法 - ValidityLifetime string `json:"validityLifetime,omitempty"` // 证书有效期,形如 "30d"、"6h" + KeySource string `json:"keySource"` // 私钥来源,可取值 "auto"、"reuse"、"custom"(零值时默认值 "auto") + KeyAlgorithm string `json:"keyAlgorithm,omitempty"` // 私钥算法 + KeyContent string `json:"keyContent,omitempty"` // 私钥内容 + ValidityLifetime string `json:"validityLifetime,omitempty"` // 有效期,形如 "30d"、"6h" ACMEProfile string `json:"acmeProfile,omitempty"` // ACME Profiles Extension Nameservers []string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔 DnsPropagationWait int32 `json:"dnsPropagationWait,omitempty"` // DNS 传播等待时间,等同于 lego 的 `--dns-propagation-wait` 参数 @@ -234,7 +238,7 @@ type WorkflowNodeConfigForBizApply struct { } type WorkflowNodeConfigForBizUpload struct { - Source string `json:"source"` // 证书来源(零值时默认值 "form") + Source string `json:"source"` // 证书来源,可取值 "form"、"local"、"url"(零值时默认值 "form") Certificate string `json:"certificate"` // 证书,根据证书来源决定是 PEM 内容 / 文件路径 / URL PrivateKey string `json:"privateKey"` // 私钥,根据证书来源决定是 PEM 内容 / 文件路径 / URL } diff --git a/internal/workflow/engine/executor_bizapply.go b/internal/workflow/engine/executor_bizapply.go index adb4a482..297f8505 100644 --- a/internal/workflow/engine/executor_bizapply.go +++ b/internal/workflow/engine/executor_bizapply.go @@ -33,6 +33,12 @@ func init() { } } +const ( + BizApplyKeySourceAuto = "auto" + BizApplyKeySourceReuse = "reuse" + BizApplyKeySourceCustom = "custom" +) + /** * Outputs: * - ref: "certificate": string @@ -203,10 +209,15 @@ func (ne *bizApplyNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, last } func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nodeCfg *domain.WorkflowNodeConfigForBizApply, lastCertificate *domain.Certificate) (*certapply.ObtainCertificateResponse, error) { - // 读取证书算法 + // 读取私钥算法 + // 如果复用私钥,则保持算法一致 legoKeyType, err := domain.CertificateKeyAlgorithmType(nodeCfg.KeyAlgorithm).KeyType() if err != nil { return nil, err + } else { + if nodeCfg.KeySource == BizApplyKeySourceReuse && lastCertificate != nil { + legoKeyType, _ = lastCertificate.KeyAlgorithm.KeyType() + } } // 读取质询提供商授权 @@ -256,8 +267,30 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod // 构造证书申请请求 obtainReq := &certapply.ObtainCertificateRequest{ - Domains: nodeCfg.Domains, - KeyType: legoKeyType, + Domains: nodeCfg.Domains, + PrivateKeyType: legoKeyType, + PrivateKeyPEM: lo. + If(nodeCfg.KeySource == BizApplyKeySourceAuto, ""). + ElseF(func() string { + switch nodeCfg.KeySource { + case BizApplyKeySourceReuse: + if lastCertificate != nil { + return lastCertificate.PrivateKey + } + case BizApplyKeySourceCustom: + return nodeCfg.KeyContent + } + return "" + }), + ValidityTo: lo. + If(nodeCfg.ValidityLifetime == "", time.Time{}). + ElseF(func() time.Time { + duration, err := str2duration.ParseDuration(nodeCfg.ValidityLifetime) + if err != nil { + return time.Time{} + } + return time.Now().Add(duration) + }), ChallengeType: nodeCfg.ChallengeType, Provider: nodeCfg.Provider, ProviderAccessConfig: providerAccessConfig, @@ -268,24 +301,17 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout, DnsTTL: nodeCfg.DnsTTL, HttpDelayWait: nodeCfg.HttpDelayWait, - ValidityTo: lo.If(nodeCfg.ValidityLifetime == "", time.Time{}). - ElseF(func() time.Time { - duration, err := str2duration.ParseDuration(nodeCfg.ValidityLifetime) - if err != nil { - return time.Time{} - } - return time.Now().Add(duration) - }), - ACMEProfile: nodeCfg.ACMEProfile, - ARIReplacesAcctUrl: lo.If(lastCertificate == nil, ""). + ACMEProfile: nodeCfg.ACMEProfile, + ARIReplacesAcctUrl: lo. + If(lastCertificate == nil, ""). ElseF(func() string { if lastCertificate.IsRenewed { return "" } - return lastCertificate.ACMEAcctUrl }), - ARIReplacesCertId: lo.If(lastCertificate == nil, ""). + ARIReplacesCertId: lo. + If(lastCertificate == nil, ""). ElseF(func() string { if lastCertificate.IsRenewed { return "" diff --git a/migrations/1760486400_m0.4.1.go b/migrations/1760486400_m0.4.1.go index 18bc922a..985f3bac 100644 --- a/migrations/1760486400_m0.4.1.go +++ b/migrations/1760486400_m0.4.1.go @@ -11,19 +11,8 @@ func init() { tracer := NewTracer("v0.4.1") tracer.Printf("go ...") - // update collection `workflow` - // - fix #982 + // adapt to new workflow data structure { - collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") - if err != nil { - return err - } - - records, err := app.FindAllRecords(collection) - if err != nil { - return err - } - type dWorkflowNode struct { Id string `json:"id"` Type string `json:"type"` @@ -71,48 +60,62 @@ func init() { return nodes, migrated } - for _, record := range records { - changed := false + // update collection `workflow` + // - fix #982 + { + collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") + if err != nil { + return err + } - graphDraft := make(map[string]any) - if err := record.UnmarshalJSONField("graphDraft", &graphDraft); err == nil { - if _, ok := graphDraft["nodes"]; ok { - nodes := make([]*dWorkflowNode, 0) - if err := mapstructure.Decode(graphDraft["nodes"], &nodes); err != nil { + records, err := app.FindAllRecords(collection) + if err != nil { + return err + } + + for _, record := range records { + changed := false + + graphDraft := make(map[string]any) + if err := record.UnmarshalJSONField("graphDraft", &graphDraft); err == nil { + if _, ok := graphDraft["nodes"]; ok { + nodes := make([]*dWorkflowNode, 0) + if err := mapstructure.Decode(graphDraft["nodes"], &nodes); err != nil { + return err + } + + if newNodes, migrated := deepMigrateNodes(nodes); migrated { + graphDraft["nodes"] = newNodes + record.Set("graphDraft", graphDraft) + changed = true + } + } + } + + graphContent := make(map[string]any) + if err := record.UnmarshalJSONField("graphContent", &graphContent); err == nil { + if _, ok := graphContent["nodes"]; ok { + nodes := make([]*dWorkflowNode, 0) + if err := mapstructure.Decode(graphContent["nodes"], &nodes); err != nil { + return err + } + + if newNodes, migrated := deepMigrateNodes(nodes); migrated { + graphContent["nodes"] = newNodes + record.Set("graphContent", graphContent) + changed = true + } + } + } + + if changed { + if err := app.Save(record); err != nil { return err } - if newNodes, migrated := deepMigrateNodes(nodes); migrated { - graphDraft["nodes"] = newNodes - record.Set("graphDraft", graphDraft) - changed = true - } + tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } - - graphContent := make(map[string]any) - if err := record.UnmarshalJSONField("graphContent", &graphContent); err == nil { - if _, ok := graphContent["nodes"]; ok { - nodes := make([]*dWorkflowNode, 0) - if err := mapstructure.Decode(graphContent["nodes"], &nodes); err != nil { - return err - } - - if newNodes, migrated := deepMigrateNodes(nodes); migrated { - graphContent["nodes"] = newNodes - record.Set("graphContent", graphContent) - changed = true - } - } - } - - if changed { - if err := app.Save(record); err != nil { - return err - } - - tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) - } } } diff --git a/migrations/1761883200_m0.4.3.go b/migrations/1761883200_m0.4.3.go deleted file mode 100644 index 370da33e..00000000 --- a/migrations/1761883200_m0.4.3.go +++ /dev/null @@ -1,56 +0,0 @@ -package migrations - -import ( - "github.com/pocketbase/pocketbase/core" - m "github.com/pocketbase/pocketbase/migrations" -) - -func init() { - m.Register(func(app core.App) error { - tracer := NewTracer("v0.4.3") - tracer.Printf("go ...") - - // update collection `certificate` - // - rename field `acmeRenewed` to `isRenewed` - // - add field `isRevoked` - { - collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") - if err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(14, []byte(`{ - "hidden": false, - "id": "bool810050391", - "name": "isRenewed", - "presentable": false, - "required": false, - "system": false, - "type": "bool" - }`)); err != nil { - return err - } - - if err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{ - "hidden": false, - "id": "bool3680845581", - "name": "isRevoked", - "presentable": false, - "required": false, - "system": false, - "type": "bool" - }`)); err != nil { - return err - } - - if err := app.Save(collection); err != nil { - return err - } - } - - tracer.Printf("done") - return nil - }, func(app core.App) error { - return nil - }) -} diff --git a/migrations/1762142400_m0.4.3.go b/migrations/1762142400_m0.4.3.go new file mode 100644 index 00000000..13af8fe7 --- /dev/null +++ b/migrations/1762142400_m0.4.3.go @@ -0,0 +1,218 @@ +package migrations + +import ( + "github.com/go-viper/mapstructure/v2" + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + tracer := NewTracer("v0.4.3") + tracer.Printf("go ...") + + // update collection `certificate` + // - rename field `acmeRenewed` to `isRenewed` + // - add field `isRevoked` + { + collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") + if err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(14, []byte(`{ + "hidden": false, + "id": "bool810050391", + "name": "isRenewed", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }`)); err != nil { + return err + } + + if err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{ + "hidden": false, + "id": "bool3680845581", + "name": "isRevoked", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }`)); err != nil { + return err + } + + if err := app.Save(collection); err != nil { + return err + } + } + + // adapt to new workflow data structure + { + type dWorkflowNode struct { + Id string `json:"id"` + Type string `json:"type"` + Data map[string]any `json:"data"` + Blocks []*dWorkflowNode `json:"blocks,omitempty,omitzero"` + } + + var deepMigrateNode func(node *dWorkflowNode) (_node *dWorkflowNode, _migrated bool) + var deepMigrateNodes func(nodes []*dWorkflowNode) (_nodes []*dWorkflowNode, _migrated bool) + deepMigrateNode = func(node *dWorkflowNode) (*dWorkflowNode, bool) { + migrated := false + + if node.Type == "bizApply" { + if node.Data != nil { + if _, ok := node.Data["config"]; ok { + nodeCfg := node.Data["config"].(map[string]any) + if nodeCfg["keySource"] == nil || nodeCfg["keySource"] == "" { + nodeCfg["keySource"] = "auto" + node.Data["config"] = nodeCfg + migrated = true + } + } + } + } else if node.Type == "bizUpload" { + if node.Data != nil { + if _, ok := node.Data["config"]; ok { + nodeCfg := node.Data["config"].(map[string]any) + if nodeCfg["source"] == nil || nodeCfg["source"] == "" { + nodeCfg["source"] = "form" + node.Data["config"] = nodeCfg + migrated = true + } + } + } + } + + if len(node.Blocks) > 0 { + if newBlocks, changed := deepMigrateNodes(node.Blocks); changed { + node.Blocks = newBlocks + migrated = true + } + } + + return node, migrated + } + deepMigrateNodes = func(nodes []*dWorkflowNode) ([]*dWorkflowNode, bool) { + migrated := false + + for i, node := range nodes { + if newNode, changed := deepMigrateNode(node); changed { + nodes[i] = newNode + migrated = true + } + } + + return nodes, migrated + } + + // update collection `workflow` + // - migrate field `graphDraft` / `graphContent` + { + collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") + if err != nil { + return err + } + + records, err := app.FindAllRecords(collection) + if err != nil { + return err + } + + for _, record := range records { + changed := false + + graphDraft := make(map[string]any) + if err := record.UnmarshalJSONField("graphDraft", &graphDraft); err == nil { + if _, ok := graphDraft["nodes"]; ok { + nodes := make([]*dWorkflowNode, 0) + if err := mapstructure.Decode(graphDraft["nodes"], &nodes); err != nil { + return err + } + + if newNodes, migrated := deepMigrateNodes(nodes); migrated { + graphDraft["nodes"] = newNodes + record.Set("graphDraft", graphDraft) + changed = true + } + } + } + + graphContent := make(map[string]any) + if err := record.UnmarshalJSONField("graphContent", &graphContent); err == nil { + if _, ok := graphContent["nodes"]; ok { + nodes := make([]*dWorkflowNode, 0) + if err := mapstructure.Decode(graphContent["nodes"], &nodes); err != nil { + return err + } + + if newNodes, migrated := deepMigrateNodes(nodes); migrated { + graphContent["nodes"] = newNodes + record.Set("graphContent", graphContent) + changed = true + } + } + } + + if changed { + if err := app.Save(record); err != nil { + return err + } + + tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) + } + } + } + + // update collection `workflow_run` + // - migrate field `graph` + { + collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") + if err != nil { + return err + } + + records, err := app.FindAllRecords(collection) + if err != nil { + return err + } + + for _, record := range records { + changed := false + + graph := make(map[string]any) + if err := record.UnmarshalJSONField("graph", &graph); err == nil { + if _, ok := graph["nodes"]; ok { + nodes := make([]*dWorkflowNode, 0) + if err := mapstructure.Decode(graph["nodes"], &nodes); err != nil { + return err + } + + if newNodes, migrated := deepMigrateNodes(nodes); migrated { + graph["nodes"] = newNodes + record.Set("graph", graph) + changed = true + } + } + } + + if changed { + if err := app.Save(record); err != nil { + return err + } + + tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) + } + } + } + } + + tracer.Printf("done") + return nil + }, func(app core.App) error { + return nil + }) +} diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go b/pkg/core/ssl-deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go index de91f5f5..a51fbeaa 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go @@ -15,7 +15,7 @@ import ( "github.com/certimate-go/certimate/pkg/core" sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/tencentcloud-ssl" - xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type SSLDeployerProviderConfig struct { @@ -196,7 +196,7 @@ func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, w if describeDomainsResp.Response.Domains != nil { for _, domain := range describeDomainsResp.Response.Domains { - if lo.FromPtr(domain.Product) == "cdn" && xcert.MatchHostname(wildcardDomain, lo.FromPtr(domain.Domain)) { + if lo.FromPtr(domain.Product) == "cdn" && xcerthostname.IsMatch(wildcardDomain, lo.FromPtr(domain.Domain)) { domains = append(domains, *domain.Domain) } } diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go b/pkg/core/ssl-deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go index 19dedd91..fc5c5e6c 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go @@ -15,7 +15,7 @@ import ( "github.com/certimate-go/certimate/pkg/core" sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/tencentcloud-ssl" - xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type SSLDeployerProviderConfig struct { @@ -196,7 +196,7 @@ func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, w if describeDomainsResp.Response.Domains != nil { for _, domain := range describeDomainsResp.Response.Domains { - if lo.FromPtr(domain.Product) == "ecdn" && xcert.MatchHostname(wildcardDomain, lo.FromPtr(domain.Domain)) { + if lo.FromPtr(domain.Product) == "ecdn" && xcerthostname.IsMatch(wildcardDomain, lo.FromPtr(domain.Domain)) { domains = append(domains, *domain.Domain) } } diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go index 823ec44e..18884344 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go @@ -14,7 +14,7 @@ import ( "github.com/certimate-go/certimate/pkg/core" sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/tencentcloud-ssl" - xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type SSLDeployerProviderConfig struct { @@ -121,7 +121,7 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke domains = lo.Filter(domainsInZone, func(domain string, _ int) bool { for _, configDomain := range d.config.Domains { - if xcert.MatchHostname(configDomain, domain) { + if xcerthostname.IsMatch(configDomain, domain) { return true } } diff --git a/pkg/core/ssl-deployer/providers/volcengine-cdn/volcengine_cdn.go b/pkg/core/ssl-deployer/providers/volcengine-cdn/volcengine_cdn.go index 16a60085..d02cdfbf 100644 --- a/pkg/core/ssl-deployer/providers/volcengine-cdn/volcengine_cdn.go +++ b/pkg/core/ssl-deployer/providers/volcengine-cdn/volcengine_cdn.go @@ -13,7 +13,7 @@ import ( "github.com/certimate-go/certimate/pkg/core" sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/volcengine-cdn" - xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type SSLDeployerProviderConfig struct { @@ -180,7 +180,7 @@ func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, w if listCdnDomainsResp.Data != nil { for _, domain := range listCdnDomainsResp.Data { - if xcert.MatchHostname(wildcardDomain, ve.StringValue(domain.Domain)) { + if xcerthostname.IsMatch(wildcardDomain, ve.StringValue(domain.Domain)) { domains = append(domains, ve.StringValue(domain.Domain)) } } diff --git a/pkg/core/ssl-deployer/providers/volcengine-live/volcengine_live.go b/pkg/core/ssl-deployer/providers/volcengine-live/volcengine_live.go index 2bacf3e4..5b4ebc8f 100644 --- a/pkg/core/ssl-deployer/providers/volcengine-live/volcengine_live.go +++ b/pkg/core/ssl-deployer/providers/volcengine-live/volcengine_live.go @@ -12,7 +12,7 @@ import ( "github.com/certimate-go/certimate/pkg/core" sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/volcengine-live" - xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type SSLDeployerProviderConfig struct { @@ -167,7 +167,7 @@ func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, w if listDomainDetailResp.Result.DomainList != nil { for _, domain := range listDomainDetailResp.Result.DomainList { - if xcert.MatchHostname(wildcardDomain, domain.Domain) { + if xcerthostname.IsMatch(wildcardDomain, domain.Domain) { domains = append(domains, domain.Domain) } } diff --git a/pkg/utils/cert/hostname.go b/pkg/utils/cert/hostname/hostname.go similarity index 90% rename from pkg/utils/cert/hostname.go rename to pkg/utils/cert/hostname/hostname.go index 722a6418..35e593e5 100644 --- a/pkg/utils/cert/hostname.go +++ b/pkg/utils/cert/hostname/hostname.go @@ -1,4 +1,4 @@ -package cert +package hostname import ( "crypto/x509" @@ -14,7 +14,7 @@ import ( // // 出参: // - 是否匹配。 -func MatchHostname(match, candidate string) bool { +func IsMatch(match, candidate string) bool { if match == "" || candidate == "" { return false } diff --git a/pkg/utils/cert/hostname_test.go b/pkg/utils/cert/hostname/hostname_test.go similarity index 81% rename from pkg/utils/cert/hostname_test.go rename to pkg/utils/cert/hostname/hostname_test.go index 85a2095c..b8255577 100644 --- a/pkg/utils/cert/hostname_test.go +++ b/pkg/utils/cert/hostname/hostname_test.go @@ -1,13 +1,13 @@ -package cert_test +package hostname_test import ( "testing" - xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) -func TestCertUtil_Hostname(t *testing.T) { - t.Run("MatchHostname", func(t *testing.T) { +func TestCertHostnameUtil_IsMatch(t *testing.T) { + t.Run("IsMatch", func(t *testing.T) { testCases := []struct { wildcard string target string @@ -36,7 +36,7 @@ func TestCertUtil_Hostname(t *testing.T) { } for _, tc := range testCases { - result := xcert.MatchHostname(tc.wildcard, tc.target) + result := xcerthostname.IsMatch(tc.wildcard, tc.target) status := "✓" pf := t.Logf if result != tc.expected { diff --git a/pkg/utils/crypto/key/key.go b/pkg/utils/crypto/key/key.go new file mode 100644 index 00000000..ebff5726 --- /dev/null +++ b/pkg/utils/crypto/key/key.go @@ -0,0 +1,46 @@ +package key + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "errors" +) + +type KeyAlgorithm = x509.PublicKeyAlgorithm + +func GetPublicKeyAlgorithm(pubkey crypto.PublicKey) (_algorithm KeyAlgorithm, _size int, _error error) { + switch t := pubkey.(type) { + case *rsa.PublicKey: + size := t.N.BitLen() + return x509.RSA, size, nil + + case *ecdsa.PublicKey: + size := t.Curve.Params().BitSize + return x509.ECDSA, size, nil + + case ed25519.PublicKey: + return x509.Ed25519, 256, nil + } + + return x509.UnknownPublicKeyAlgorithm, 0, errors.New("unknown public key type") +} + +func GetPrivateKeyAlgorithm(privkey crypto.PrivateKey) (_algorithm KeyAlgorithm, _size int, _error error) { + switch t := privkey.(type) { + case *rsa.PrivateKey: + size := t.N.BitLen() + return x509.RSA, size, nil + + case *ecdsa.PrivateKey: + size := t.Curve.Params().BitSize + return x509.ECDSA, size, nil + + case ed25519.PrivateKey: + return x509.Ed25519, 256, nil + } + + return x509.UnknownPublicKeyAlgorithm, 0, errors.New("unknown private key type") +} diff --git a/ui/src/api/certificates.ts b/ui/src/api/certificates.ts index 33054671..5093794f 100644 --- a/ui/src/api/certificates.ts +++ b/ui/src/api/certificates.ts @@ -3,14 +3,13 @@ import { ClientResponseError } from "pocketbase"; import { type CertificateFormatType } from "@/domain/certificate"; import { getPocketBase } from "@/repository/_pocketbase"; -type ArchiveRespData = { - fileBytes: string; -}; - export const archive = async (certificateId: string, format?: CertificateFormatType) => { const pb = getPocketBase(); - const resp = await pb.send>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, { + type RespData = { + fileBytes: string; + }; + const resp = await pb.send>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, { method: "POST", headers: { "Content-Type": "application/json", @@ -30,7 +29,7 @@ export const archive = async (certificateId: string, format?: CertificateFormatT export const revoke = async (certificateId: string) => { const pb = getPocketBase(); - const resp = await pb.send>(`/api/certificates/${encodeURIComponent(certificateId)}/revoke`, { + const resp = await pb.send(`/api/certificates/${encodeURIComponent(certificateId)}/revoke`, { method: "POST", headers: { "Content-Type": "application/json", @@ -44,14 +43,14 @@ export const revoke = async (certificateId: string) => { return resp; }; -type ValidateCertificateResp = { - isValid: boolean; - domains: string; -}; - export const validateCertificate = async (certificate: string) => { const pb = getPocketBase(); - const resp = await pb.send>(`/api/certificates/validate/certificate`, { + + type RespData = { + isValid: boolean; + domains: string; + }; + const resp = await pb.send>(`/api/certificates/validate/certificate`, { method: "POST", headers: { "Content-Type": "application/json", @@ -68,13 +67,14 @@ export const validateCertificate = async (certificate: string) => { return resp; }; -type ValidatePrivateKeyResp = { - isValid: boolean; -}; - export const validatePrivateKey = async (privateKey: string) => { const pb = getPocketBase(); - const resp = await pb.send>(`/api/certificates/validate/private-key`, { + + type RespData = { + isValid: boolean; + keyAlgorithm: string; + }; + const resp = await pb.send>(`/api/certificates/validate/private-key`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/ui/src/api/workflows.ts b/ui/src/api/workflows.ts index 81688c89..400edb4f 100644 --- a/ui/src/api/workflows.ts +++ b/ui/src/api/workflows.ts @@ -6,13 +6,12 @@ import { getPocketBase } from "@/repository/_pocketbase"; export const getStats = async () => { const pb = getPocketBase(); - const resp = await pb.send< - BaseResponse<{ - concurrency: number; - pendingRunIds: string[]; - processingRunIds: string[]; - }> - >(`/api/workflows/stats`, { + type RespData = { + concurrency: number; + pendingRunIds: string[]; + processingRunIds: string[]; + }; + const resp = await pb.send>(`/api/workflows/stats`, { method: "GET", headers: { "Content-Type": "application/json", diff --git a/ui/src/components/workflow/designer/elements/DragNode.tsx b/ui/src/components/workflow/designer/elements/DragNode.tsx index e6a2ddd6..d3c05332 100644 --- a/ui/src/components/workflow/designer/elements/DragNode.tsx +++ b/ui/src/components/workflow/designer/elements/DragNode.tsx @@ -1,4 +1,4 @@ -import { type FlowNodeEntity, type DragNodeProps as FlowgramDragNodeProps, getNodeForm } from "@flowgram.ai/fixed-layout-editor"; +import { type FlowNodeEntity, type DragNodeProps as FlowgramDragNodeProps } from "@flowgram.ai/fixed-layout-editor"; import { Badge, Card } from "antd"; export interface DragNodeProps extends FlowgramDragNodeProps { @@ -15,7 +15,7 @@ const DragNode = ({ dragStart, dragNodes }: DragNodeProps) => {
-
{dragStart ? getNodeForm(dragStart)?.getValueIn("name") || `#${dragStart?.id}` : "\u00A0"}
+
{dragStart ? dragStart.form?.getValueIn("name") || `#${dragStart?.id}` : "\u00A0"}
diff --git a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx index 9c99cf64..58d167b6 100644 --- a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx @@ -23,6 +23,7 @@ import { import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import { validatePrivateKey } from "@/api/certificates"; import AccessEditDrawer from "@/components/access/AccessEditDrawer"; import AccessSelect from "@/components/access/AccessSelect"; import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; @@ -30,12 +31,14 @@ import ACMEDns01ProviderSelect from "@/components/provider/ACMEDns01ProviderSele import ACMEHttp01ProviderSelect from "@/components/provider/ACMEHttp01ProviderSelect"; import CAProviderSelect from "@/components/provider/CAProviderSelect"; import Show from "@/components/Show"; +import TextFileInput from "@/components/TextFileInput"; import { type AccessModel } from "@/domain/access"; import { ACME_DNS01_PROVIDERS, ACME_HTTP01_PROVIDERS, acmeDns01ProvidersMap, acmeHttp01ProvidersMap, caProvidersMap } from "@/domain/provider"; import { type WorkflowNodeConfigForBizApply, defaultNodeConfigForBizApply } from "@/domain/workflow"; import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; import { useContactEmailsStore } from "@/stores/contact"; +import { getErrMsg } from "@/utils/error"; import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/validators"; import { FormNestedFieldsContextProvider, NodeFormContextProvider } from "./_context"; @@ -53,6 +56,10 @@ const MULTIPLE_INPUT_SEPARATOR = ";"; const CHALLENGE_TYPE_DNS01 = "dns-01"; const CHALLENGE_TYPE_HTTP01 = "http-01"; +const KEY_SOURCE_AUTO = "auto" as const; +const KEY_SOURCE_REUSE = "reuse" as const; +const KEY_SOURCE_CUSTOM = "custom" as const; + export interface BizApplyNodeConfigFormProps { form: FormInstance; node: FlowNodeEntity; @@ -92,9 +99,18 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) const fieldChallengeType = Form.useWatch("challengeType", { form: formInst, preserve: true }); const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); const fieldProviderAccessId = Form.useWatch("providerAccessId", { form: formInst, preserve: true }); + const fieldKeySource = Form.useWatch("keySource", { form: formInst, preserve: true }); const fieldCAProvider = Form.useWatch("caProvider", { form: formInst, preserve: true }); const fieldCAProviderAccessId = Form.useWatch("caProviderAccessId", { form: formInst, preserve: true }); + const resetFieldIfInvalid = (field: keyof z.infer) => { + const fieldSchame = formSchema.pick({ [field]: true }); + const fieldValue = formInst.getFieldValue(field); + if (!fieldSchame.safeParse({ [field]: fieldValue }).success) { + formInst.setFieldValue(field, void 0); + } + }; + const NestedProviderConfigFields = useMemo(() => { /* 注意:如果追加新的子组件,请保持以 ASCII 排序。 @@ -225,14 +241,6 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) }, [fieldCAProvider, fieldCAProviderAccessId]); const handleChallengeTypeChange = (value: string) => { - const resetFieldIfInvalid = (field: keyof z.infer) => { - const fieldSchame = formSchema.pick({ [field]: true }); - const fieldValue = formInst.getFieldValue(field); - if (!fieldSchame.safeParse({ [field]: fieldValue }).success) { - formInst.setFieldValue(field, void 0); - } - }; - switch (value) { case CHALLENGE_TYPE_DNS01: { @@ -267,6 +275,40 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) } }; + const handleKeySourceChange = (value: string) => { + if (value === initialValues?.keySource) { + formInst.resetFields(["keyContent"]); + } else { + setTimeout(() => { + formInst.setFieldValue("keyContent", ""); + }, 0); + } + }; + + const handleKeyContentChange = async (value: string) => { + try { + const resp = await validatePrivateKey(value); + formInst.setFields([ + { + name: "keyContent", + value: value, + }, + { + name: "keyAlgorithm", + value: resp.data.keyAlgorithm, + }, + ]); + } catch (e) { + formInst.setFields([ + { + name: "keyContent", + value: value, + errors: [getErrMsg(e)], + }, + ]); + } + }; + const handleCAProviderSelect = (value?: string | undefined) => { // 切换 CA 提供商时联动授权信息 if (value == null || value === "") { @@ -429,7 +471,28 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) - + + handleKeySourceChange(e.target.value)}> + {t("workflow_node.apply.form.key_source.option.auto.label")} + {t("workflow_node.apply.form.key_source.option.reuse.label")} + {t("workflow_node.apply.form.key_source.option.custom.label")} + + + + + ) : fieldKeySource === KEY_SOURCE_CUSTOM ? ( + + ) : ( + void 0 + ) + } + rules={[formRule]} + >