From b43fcc3b613e0317c7e64d87bc8ac3f625c74e75 Mon Sep 17 00:00:00 2001 From: JiangJam <39955422+JiangJamm@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:56:50 +0800 Subject: [PATCH 1/8] fix: wrong path when uploading certificates from Windows to Unix fix: https://github.com/certimate-go/certimate/issues/805 --- pkg/core/ssl-deployer/providers/ssh/ssh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/ssl-deployer/providers/ssh/ssh.go b/pkg/core/ssl-deployer/providers/ssh/ssh.go index 94d0b6bf9..7cbe7c8f3 100644 --- a/pkg/core/ssl-deployer/providers/ssh/ssh.go +++ b/pkg/core/ssl-deployer/providers/ssh/ssh.go @@ -419,7 +419,7 @@ func writeFileWithSFTP(sshCli *ssh.Client, path string, data []byte) error { } defer sftpCli.Close() - if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil { + if err := sftpCli.MkdirAll(filepath.ToSlash(filepath.Dir(path))); err != nil { return fmt.Errorf("failed to create remote directory: %w", err) } From 1e4cd2b9d5fb417abe1daac59b35b711d74dfd62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=83=E7=93=9C=E7=9A=84=E6=98=9F=E6=A0=B8=E7=B2=BE?= <129462191+cgxhj@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:22:29 +0800 Subject: [PATCH 2/8] feat: support configuring multiple domains deployment to tencentcloud edgeone --- internal/deployer/providers.go | 2 +- migrations/1742209200_upgrade.go | 10 +- migrations/1751961600_upgrade.go | 112 ++++++++++++++++++ .../tencentcloud-eo/tencentcloud_eo.go | 10 +- .../tencentcloud-eo/tencentcloud_eo_test.go | 10 +- ...ployNodeConfigFormTencentCloudEOConfig.tsx | 29 +++-- .../i18n/locales/en/nls.workflow.nodes.json | 8 +- .../i18n/locales/zh/nls.workflow.nodes.json | 8 +- 8 files changed, 159 insertions(+), 30 deletions(-) create mode 100644 migrations/1751961600_upgrade.go diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 0a48d6ca7..7eb583726 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -1202,7 +1202,7 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy SecretKey: access.SecretKey, Endpoint: xmaps.GetString(options.ProviderServiceConfig, "endpoint"), ZoneId: xmaps.GetString(options.ProviderServiceConfig, "zoneId"), - Domain: xmaps.GetString(options.ProviderServiceConfig, "domain"), + Domains: xslices.Filter(strings.Split(xmaps.GetString(options.ProviderServiceConfig, "domains"), ";"), func(s string) bool { return s != "" }), }) return deployer, err diff --git a/migrations/1742209200_upgrade.go b/migrations/1742209200_upgrade.go index 1bc0d4827..1cc05cf3f 100644 --- a/migrations/1742209200_upgrade.go +++ b/migrations/1742209200_upgrade.go @@ -270,12 +270,12 @@ func init() { Id string `json:"id"` Type string `json:"type"` Name string `json:"name"` - Config map[string]any `json:"config"` - Inputs []map[string]any `json:"inputs"` - Outputs []map[string]any `json:"outputs"` + Config map[string]any `json:"config,omitempty"` + Inputs []map[string]any `json:"inputs,omitempty"` + Outputs []map[string]any `json:"outputs,omitempty"` Next *dWorkflowNode `json:"next,omitempty"` - Branches []dWorkflowNode `json:"branches,omitempty"` - Validated bool `json:"validated"` + Branches []*dWorkflowNode `json:"branches,omitempty"` + Validated bool `json:"validated,omitempty"` } for _, workflowRun := range workflowRuns { diff --git a/migrations/1751961600_upgrade.go b/migrations/1751961600_upgrade.go new file mode 100644 index 000000000..91ac39d26 --- /dev/null +++ b/migrations/1751961600_upgrade.go @@ -0,0 +1,112 @@ +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.3)1751961600") + tracer.Printf("go ...") + + // migrate data + { + workflows, err := app.FindAllRecords("workflow") + if err != nil { + return err + } + + type dWorkflowNode struct { + Id string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Config map[string]any `json:"config,omitempty"` + Inputs []map[string]any `json:"inputs,omitempty"` + Outputs []map[string]any `json:"outputs,omitempty"` + Next *dWorkflowNode `json:"next,omitempty"` + Branches []*dWorkflowNode `json:"branches,omitempty"` + Validated bool `json:"validated,omitempty"` + } + + deepChangeFn := func(node *dWorkflowNode) bool { + stack := []*dWorkflowNode{node} + + for len(stack) > 0 { + current := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if current.Type == "deploy" { + configMap := current.Config + if configMap != nil { + if provider, ok := configMap["provider"]; ok { + if provider.(string) == "tencentcloud-eo" { + if providerConfig, ok := configMap["providerConfig"]; ok { + if providerConfigMap, ok := providerConfig.(map[string]any); ok { + if _, ok := providerConfigMap["domain"]; ok { + providerConfigMap["domains"] = providerConfigMap["domain"] + delete(providerConfigMap, "domain") + configMap["providerConfig"] = providerConfigMap + return true + } + } + } + } + } + } + } + + if current.Next != nil { + stack = append(stack, current.Next) + } + + if current.Branches != nil { + for i := len(current.Branches) - 1; i >= 0; i-- { + stack = append(stack, current.Branches[i]) + } + } + } + + return false + } + + for _, workflow := range workflows { + changed := false + + rootNodeContent := &dWorkflowNode{} + if err := workflow.UnmarshalJSONField("content", rootNodeContent); err != nil { + return err + } else { + if deepChangeFn(rootNodeContent) { + workflow.Set("content", rootNodeContent) + changed = true + } + } + + rootNodeDraft := &dWorkflowNode{} + if err := workflow.UnmarshalJSONField("draft", rootNodeDraft); err != nil { + return err + } else { + if deepChangeFn(rootNodeDraft) { + workflow.Set("draft", rootNodeDraft) + changed = true + } + } + + if changed { + err = app.Save(workflow) + if err != nil { + return err + } + + tracer.Printf("record #%s in collection '%s' updated", workflow.Id, workflow.Collection().Name) + } + } + } + + tracer.Printf("done") + return nil + }, func(app core.App) error { + return nil + }) +} 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 5540c8c84..e2fcaf7d4 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go @@ -25,8 +25,8 @@ type SSLDeployerProviderConfig struct { Endpoint string `json:"endpoint,omitempty"` // 站点 ID。 ZoneId string `json:"zoneId"` - // 加速域名(支持泛域名)。 - Domain string `json:"domain"` + // 加速域名列表(支持泛域名)。 + Domains []string `json:"domains"` } type SSLDeployerProvider struct { @@ -82,8 +82,8 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke if d.config.ZoneId == "" { return nil, errors.New("config `zoneId` is required") } - if d.config.Domain == "" { - return nil, errors.New("config `domain` is required") + if len(d.config.Domains) == 0 { + return nil, errors.New("config `domains` is required") } // 上传证书 @@ -99,7 +99,7 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke modifyHostsCertificateReq := tcteo.NewModifyHostsCertificateRequest() modifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId) modifyHostsCertificateReq.Mode = common.StringPtr("sslcert") - modifyHostsCertificateReq.Hosts = common.StringPtrs([]string{d.config.Domain}) + modifyHostsCertificateReq.Hosts = common.StringPtrs(d.config.Domains) modifyHostsCertificateReq.ServerCertInfo = []*tcteo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}} modifyHostsCertificateResp, err := d.sdkClient.ModifyHostsCertificate(modifyHostsCertificateReq) d.logger.Debug("sdk request 'teo.ModifyHostsCertificate'", slog.Any("request", modifyHostsCertificateReq), slog.Any("response", modifyHostsCertificateResp)) diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go index cbca3fe3a..51a440c1b 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go @@ -17,7 +17,7 @@ var ( fSecretId string fSecretKey string fZoneId string - fDomain string + fDomains string ) func init() { @@ -28,7 +28,7 @@ func init() { flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fZoneId, argsPrefix+"ZONEID", "", "") - flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") + flag.StringVar(&fDomains, argsPrefix+"DOMAINS", "", "") } /* @@ -40,7 +40,7 @@ Shell command to run this test: --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_SECRETID="your-secret-id" \ --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_SECRETKEY="your-secret-key" \ --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_ZONEID="your-zone-id" \ - --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_DOMAIN="example.com" + --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_DOMAINS="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() @@ -53,14 +53,14 @@ func TestDeploy(t *testing.T) { fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("ZONEID: %v", fZoneId), - fmt.Sprintf("DOMAIN: %v", fDomain), + fmt.Sprintf("DOMAINS: %v", fDomains), }, "\n")) deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{ SecretId: fSecretId, SecretKey: fSecretKey, ZoneId: fZoneId, - Domain: fDomain, + Domains: strings.Split(fDomains, ";"), }) if err != nil { t.Errorf("err: %+v", err) diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudEOConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudEOConfig.tsx index b5daf7158..7702c39ae 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudEOConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudEOConfig.tsx @@ -4,11 +4,12 @@ import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod/v4"; import { validDomainName } from "@/utils/validators"; +import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; type DeployNodeConfigFormTencentCloudEOConfigFieldValues = Nullish<{ endpoint?: string; zoneId: string; - domain: string; + domains: string; }>; export type DeployNodeConfigFormTencentCloudEOConfigProps = { @@ -23,6 +24,8 @@ const initFormModel = (): DeployNodeConfigFormTencentCloudEOConfigFieldValues => return {}; }; +const MULTIPLE_INPUT_SEPARATOR = ";"; + const DeployNodeConfigFormTencentCloudEOConfig = ({ form: formInst, formName, @@ -37,9 +40,14 @@ const DeployNodeConfigFormTencentCloudEOConfig = ({ zoneId: z .string(t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder")) .nonempty(t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder")), - domain: z - .string(t("workflow_node.deploy.form.tencentcloud_eo_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), + domains: z + .string(t("workflow_node.deploy.form.tencentcloud_eo_domains.placeholder")) + .refine((v) => { + if (!v) return false; + return String(v) + .split(MULTIPLE_INPUT_SEPARATOR) + .every((e) => validDomainName(e, { allowWildcard: true })); + }, t("common.errmsg.domain_invalid")), }); const formRule = createSchemaFieldRule(formSchema); @@ -75,12 +83,17 @@ const DeployNodeConfigFormTencentCloudEOConfig = ({ } + tooltip={} > - + ); diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 122542213..df7f19152 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -727,9 +727,11 @@ "workflow_node.deploy.form.tencentcloud_eo_zone_id.label": "Tencent Cloud EdgeOne zone ID", "workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder": "Please enter Tencent Cloud EdgeOne zone ID", "workflow_node.deploy.form.tencentcloud_eo_zone_id.tooltip": "For more information, see https://console.tencentcloud.com/edgeone", - "workflow_node.deploy.form.tencentcloud_eo_domain.label": "Tencent Cloud EdgeOne domain", - "workflow_node.deploy.form.tencentcloud_eo_domain.placeholder": "Please enter Tencent Cloud EdgeOne domain name", - "workflow_node.deploy.form.tencentcloud_eo_domain.tooltip": "For more information, see https://console.tencentcloud.com/edgeone", + "workflow_node.deploy.form.tencentcloud_eo_domains.label": "Tencent Cloud EdgeOne domains", + "workflow_node.deploy.form.tencentcloud_eo_domains.placeholder": "Please enter Tencent Cloud EdgeOne domain names (separated by semicolons)", + "workflow_node.deploy.form.tencentcloud_eo_domains.tooltip": "For more information, see https://console.tencentcloud.com/edgeone", + "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title": "Change Tencent Cloud EdgeOne domain", + "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder": "Please enter Tencent Cloud EdgeOne domain name", "workflow_node.deploy.form.tencentcloud_gaap_endpoint.label": "Tencent Cloud GAAP API endpoint (Optional)", "workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder": "Please enter Tencent Cloud GAAP API endpoint (e.g. gaap.intl.tencentcloudapi.com)", "workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip": "", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index a3f4278e7..061189dfd 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -725,9 +725,11 @@ "workflow_node.deploy.form.tencentcloud_eo_zone_id.label": "腾讯云 EdgeOne 站点 ID", "workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder": "请输入腾讯云 EdgeOne 站点 ID", "workflow_node.deploy.form.tencentcloud_eo_zone_id.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone", - "workflow_node.deploy.form.tencentcloud_eo_domain.label": "腾讯云 EdgeOne 加速域名", - "workflow_node.deploy.form.tencentcloud_eo_domain.placeholder": "请输入腾讯云 EdgeOne 加速域名(支持泛域名)", - "workflow_node.deploy.form.tencentcloud_eo_domain.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone", + "workflow_node.deploy.form.tencentcloud_eo_domains.label": "腾讯云 EdgeOne 加速域名", + "workflow_node.deploy.form.tencentcloud_eo_domains.placeholder": "请输入腾讯云 EdgeOne 加速域名(支持泛域名;多个值请用半角分号隔开)", + "workflow_node.deploy.form.tencentcloud_eo_domains.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone", + "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title": "修改腾讯云 EdgeOne 加速域名", + "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder": "请输入腾讯云 EdgeOne 加速域名(支持泛域名)", "workflow_node.deploy.form.tencentcloud_gaap_endpoint.label": "腾讯云 GAAP 接口端点(可选)", "workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder": "请输入腾讯云 GAAP 接口端点(例如:gaap.tencentcloudapi.com)", "workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/608/36934

国际站用户请填写 gaap.intl.tencentcloudapi.com。", From b6c59c57ded445c0e8916a7ff5e63f6759c13bf7 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sun, 6 Jul 2025 22:47:13 +0800 Subject: [PATCH 3/8] fix: #852 --- pkg/sdk3rd/baishan/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sdk3rd/baishan/client.go b/pkg/sdk3rd/baishan/client.go index fb9a1df79..2978cd33e 100644 --- a/pkg/sdk3rd/baishan/client.go +++ b/pkg/sdk3rd/baishan/client.go @@ -22,7 +22,7 @@ func NewClient(apiToken string) (*Client, error) { SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", "certimate"). - SetHeader("Token", apiToken) + SetQueryParam("token", apiToken) return &Client{client}, nil } From 72cfe921d3004544679c9bdfd2ac497ebcb87204 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sun, 6 Jul 2025 23:07:07 +0800 Subject: [PATCH 4/8] fix: #849 --- pkg/sdk3rd/upyun/console/client.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/sdk3rd/upyun/console/client.go b/pkg/sdk3rd/upyun/console/client.go index 7af3e7ae7..0c54ea613 100644 --- a/pkg/sdk3rd/upyun/console/client.go +++ b/pkg/sdk3rd/upyun/console/client.go @@ -103,8 +103,11 @@ func (c *Client) doRequestWithResult(req *resty.Request, res apiResponse) (*rest if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w", err) } else { - if tdata := res.GetData(); tdata == nil { - return resp, fmt.Errorf("sdkerr: empty data") + tresp := &apiResponseBase{} + if err := json.Unmarshal(resp.Body(), &tresp); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w", err) + } else if tdata := tresp.GetData(); tdata == nil { + return resp, fmt.Errorf("sdkerr: received empty data") } else if terrcode := tdata.GetErrorCode(); terrcode != 0 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", terrcode, tdata.GetMessage()) } From 424e2e7710f2c47f6f1dd54ce31a3f0c8655a079 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 7 Jul 2025 10:04:14 +0800 Subject: [PATCH 5/8] build: fix README release path --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed10b5ee0..4f8efd8b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -189,6 +189,8 @@ jobs: cp ../README.md "$tmpdir/README_zhCN.md" cp ../README_EN.md "$tmpdir/README_enUS.md" cp ../CHANGELOG.md "$tmpdir/CHANGELOG.md" + sed -i 's/README_EN\.md/README_enUS.md/g' "$tmpdir/README_zhCN.md" + sed -i 's/README\.md/README_zhCN.md/g' "$tmpdir/README_enUS.md" if [[ "$bin" == *".exe" ]]; then zip -j "${bin%.exe}.zip" "$tmpdir"/* From 7d272a6c524daf05c770161967fc5c1b4d8099ca Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 7 Jul 2025 17:27:04 +0800 Subject: [PATCH 6/8] fix: missing pfx certificiate chains --- pkg/utils/cert/common.go | 16 +++++++++ pkg/utils/cert/extractor.go | 26 +++++++-------- pkg/utils/cert/transformer.go | 62 +++++++++++++++++++++++------------ 3 files changed, 68 insertions(+), 36 deletions(-) diff --git a/pkg/utils/cert/common.go b/pkg/utils/cert/common.go index 567031252..bdc863673 100644 --- a/pkg/utils/cert/common.go +++ b/pkg/utils/cert/common.go @@ -2,6 +2,7 @@ package cert import ( "crypto/x509" + "encoding/pem" ) // 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。 @@ -24,3 +25,18 @@ func EqualCertificate(a, b *x509.Certificate) bool { a.Issuer.SerialNumber == b.Issuer.SerialNumber && a.Subject.SerialNumber == b.Subject.SerialNumber } + +func decodePEM(data []byte) []*pem.Block { + blocks := make([]*pem.Block, 0) + for { + block, rest := pem.Decode(data) + if block == nil { + break + } + + blocks = append(blocks, block) + data = rest + } + + return blocks +} diff --git a/pkg/utils/cert/extractor.go b/pkg/utils/cert/extractor.go index 1e116b1fd..b9e4607f1 100644 --- a/pkg/utils/cert/extractor.go +++ b/pkg/utils/cert/extractor.go @@ -3,6 +3,7 @@ package cert import ( "encoding/pem" "errors" + "fmt" ) // 从 PEM 编码的证书字符串解析并提取服务器证书和中间证书。 @@ -15,32 +16,27 @@ import ( // - intermediaCertPEM: 中间证书的 PEM 内容。 // - err: 错误。 func ExtractCertificatesFromPEM(certPEM string) (_serverCertPEM string, _intermediaCertPEM string, _err error) { - pemBlocks := make([]*pem.Block, 0) - pemData := []byte(certPEM) - for { - block, rest := pem.Decode(pemData) - if block == nil || block.Type != "CERTIFICATE" { - break + blocks := decodePEM([]byte(certPEM)) + for i, block := range blocks { + if block.Type != "CERTIFICATE" { + return "", "", fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, block.Type) } - - pemBlocks = append(pemBlocks, block) - pemData = rest } serverCertPEM := "" intermediaCertPEM := "" - if len(pemBlocks) == 0 { + if len(blocks) == 0 { return "", "", errors.New("failed to decode PEM block") } - if len(pemBlocks) > 0 { - serverCertPEM = string(pem.EncodeToMemory(pemBlocks[0])) + if len(blocks) > 0 { + serverCertPEM = string(pem.EncodeToMemory(blocks[0])) } - if len(pemBlocks) > 1 { - for i := 1; i < len(pemBlocks); i++ { - intermediaCertPEM += string(pem.EncodeToMemory(pemBlocks[i])) + if len(blocks) > 1 { + for i := 1; i < len(blocks); i++ { + intermediaCertPEM += string(pem.EncodeToMemory(blocks[i])) } } diff --git a/pkg/utils/cert/transformer.go b/pkg/utils/cert/transformer.go index bf467efa7..690ae19fd 100644 --- a/pkg/utils/cert/transformer.go +++ b/pkg/utils/cert/transformer.go @@ -2,8 +2,9 @@ package cert import ( "bytes" - "encoding/pem" + "crypto/x509" "errors" + "fmt" "time" "github.com/pavlo-v-chernykh/keystore-go/v4" @@ -21,9 +22,19 @@ import ( // - data: PFX 格式的证书数据。 // - err: 错误。 func TransformCertificateFromPEMToPFX(certPEM string, privkeyPEM string, pfxPassword string) ([]byte, error) { - cert, err := ParseCertificateFromPEM(certPEM) - if err != nil { - return nil, err + blocks := decodePEM([]byte(certPEM)) + + certs := make([]*x509.Certificate, 0, len(blocks)) + for i, block := range blocks { + if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, block.Type) + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + certs = append(certs, cert) } privkey, err := ParsePrivateKeyFromPEM(privkeyPEM) @@ -31,12 +42,16 @@ func TransformCertificateFromPEMToPFX(certPEM string, privkeyPEM string, pfxPass return nil, err } - pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, pfxPassword) - if err != nil { - return nil, err + var pfxData []byte + if len(certs) == 0 { + return nil, errors.New("failed to decode certificate PEM") + } else if len(certs) == 1 { + pfxData, err = pkcs12.Legacy.Encode(privkey, certs[0], nil, pfxPassword) + } else { + pfxData, err = pkcs12.Legacy.Encode(privkey, certs[0], certs[1:], pfxPassword) } - return pfxData, nil + return pfxData, err } // 将 PEM 编码的证书字符串转换为 JKS 格式。 @@ -52,28 +67,33 @@ func TransformCertificateFromPEMToPFX(certPEM string, privkeyPEM string, pfxPass // - data: JKS 格式的证书数据。 // - err: 错误。 func TransformCertificateFromPEMToJKS(certPEM string, privkeyPEM string, jksAlias string, jksKeypass string, jksStorepass string) ([]byte, error) { - certBlock, _ := pem.Decode([]byte(certPEM)) - if certBlock == nil { + certBlocks := decodePEM([]byte(certPEM)) + if len(certBlocks) == 0 { return nil, errors.New("failed to decode certificate PEM") } - privkeyBlock, _ := pem.Decode([]byte(privkeyPEM)) - if privkeyBlock == nil { + privkeyBlocks := decodePEM([]byte(privkeyPEM)) + if len(privkeyBlocks) == 0 { return nil, errors.New("failed to decode private key PEM") } - ks := keystore.New() entry := keystore.PrivateKeyEntry{ - CreationTime: time.Now(), - PrivateKey: privkeyBlock.Bytes, - CertificateChain: []keystore.Certificate{ - { - Type: "X509", - Content: certBlock.Bytes, - }, - }, + CreationTime: time.Now(), + PrivateKey: privkeyBlocks[0].Bytes, + CertificateChain: make([]keystore.Certificate, len(certBlocks)), + } + for i, certBlock := range certBlocks { + if certBlock.Type != "CERTIFICATE" { + return nil, fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, certBlock.Type) + } + + entry.CertificateChain[i] = keystore.Certificate{ + Type: "X509", + Content: certBlock.Bytes, + } } + ks := keystore.New() if err := ks.SetPrivateKeyEntry(jksAlias, entry, []byte(jksKeypass)); err != nil { return nil, err } From 2f551dd3e3db9b5ff417f6b2ee1ff64f7e36af4a Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 8 Jul 2025 10:25:44 +0800 Subject: [PATCH 7/8] fix: #854 --- internal/deployer/providers.go | 11 ++- .../providers/qiniu-kodo/qiniu_kodo.go | 88 +++++++++++++++++++ .../providers/qiniu-kodo/qiniu_kodo_test.go | 75 ++++++++++++++++ .../providers/qiniu-sslcert/qiniu_sslcert.go | 81 +++++++++++++++-- .../qiniu-sslcert/qiniu_sslcert_test.go | 72 +++++++++++++++ pkg/sdk3rd/qiniu/cdn.go | 45 +--------- pkg/sdk3rd/qiniu/kodo.go | 44 ++++++++++ pkg/sdk3rd/qiniu/sslcert.go | 80 +++++++++++++++++ pkg/sdk3rd/qiniu/util.go | 14 +++ .../i18n/locales/en/nls.workflow.nodes.json | 28 +++--- .../i18n/locales/zh/nls.workflow.nodes.json | 8 +- 11 files changed, 481 insertions(+), 65 deletions(-) create mode 100644 pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo.go create mode 100644 pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo_test.go create mode 100644 pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert_test.go create mode 100644 pkg/sdk3rd/qiniu/kodo.go create mode 100644 pkg/sdk3rd/qiniu/sslcert.go create mode 100644 pkg/sdk3rd/qiniu/util.go diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 7eb583726..61ae87859 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -69,6 +69,7 @@ import ( pNetlifySite "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/netlify-site" pProxmoxVE "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/proxmoxve" pQiniuCDN "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-cdn" + pQiniuKodo "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-kodo" pQiniuPili "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-pili" pRainYunRCDN "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/rainyun-rcdn" pRatPanelConsole "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/ratpanel-console" @@ -1001,7 +1002,7 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy } switch options.Provider { - case domain.DeploymentProviderTypeQiniuCDN, domain.DeploymentProviderTypeQiniuKodo: + case domain.DeploymentProviderTypeQiniuCDN: deployer, err := pQiniuCDN.NewSSLDeployerProvider(&pQiniuCDN.SSLDeployerProviderConfig{ AccessKey: access.AccessKey, SecretKey: access.SecretKey, @@ -1009,6 +1010,14 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy }) return deployer, err + case domain.DeploymentProviderTypeQiniuKodo: + deployer, err := pQiniuKodo.NewSSLDeployerProvider(&pQiniuKodo.SSLDeployerProviderConfig{ + AccessKey: access.AccessKey, + SecretKey: access.SecretKey, + Domain: xmaps.GetString(options.ProviderServiceConfig, "domain"), + }) + return deployer, err + case domain.DeploymentProviderTypeQiniuPili: deployer, err := pQiniuPili.NewSSLDeployerProvider(&pQiniuPili.SSLDeployerProviderConfig{ AccessKey: access.AccessKey, diff --git a/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo.go b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo.go new file mode 100644 index 000000000..7dcec1724 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo.go @@ -0,0 +1,88 @@ +package qiniukodo + +import ( + "context" + "errors" + "fmt" + "log/slog" + + "github.com/qiniu/go-sdk/v7/auth" + + "github.com/certimate-go/certimate/pkg/core" + sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/qiniu-sslcert" + qiniusdk "github.com/certimate-go/certimate/pkg/sdk3rd/qiniu" +) + +type SSLDeployerProviderConfig struct { + // 七牛云 AccessKey。 + AccessKey string `json:"accessKey"` + // 七牛云 SecretKey。 + SecretKey string `json:"secretKey"` + // 自定义域名(不支持泛域名)。 + Domain string `json:"domain"` +} + +type SSLDeployerProvider struct { + config *SSLDeployerProviderConfig + logger *slog.Logger + sdkClient *qiniusdk.KodoManager + sslManager core.SSLManager +} + +var _ core.SSLDeployer = (*SSLDeployerProvider)(nil) + +func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the ssl deployer provider is nil") + } + + client := qiniusdk.NewKodoManager(auth.New(config.AccessKey, config.SecretKey)) + + sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{ + AccessKey: config.AccessKey, + SecretKey: config.SecretKey, + }) + if err != nil { + return nil, fmt.Errorf("could not create ssl manager: %w", err) + } + + return &SSLDeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + sslManager: sslmgr, + }, nil +} + +func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } + + d.sslManager.SetLogger(logger) +} + +func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) { + if d.config.Domain == "" { + return nil, fmt.Errorf("config `domain` is required") + } + + // 上传证书 + upres, err := d.sslManager.Upload(ctx, certPEM, privkeyPEM) + if err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + // 绑定空间域名证书 + bindBucketCertResp, err := d.sdkClient.BindBucketCert(context.TODO(), d.config.Domain, upres.CertId) + d.logger.Debug("sdk request 'kodo.BindCert'", slog.String("request.domain", d.config.Domain), slog.String("request.certId", upres.CertId), slog.Any("response", bindBucketCertResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'kodo.BindCert': %w", err) + } + + return &core.SSLDeployResult{}, nil +} diff --git a/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo_test.go b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo_test.go new file mode 100644 index 000000000..3dfcf4561 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo_test.go @@ -0,0 +1,75 @@ +package qiniukodo_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-kodo" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKey string + fSecretKey string + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_SSLDEPLOYER_QINIUKODO_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") + flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") +} + +/* +Shell command to run this test: + + go test -v ./qiniu_kodo_test.go -args \ + --CERTIMATE_SSLDEPLOYER_QINIUKODO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_SSLDEPLOYER_QINIUKODO_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_SSLDEPLOYER_QINIUKODO_ACCESSKEY="your-access-key" \ + --CERTIMATE_SSLDEPLOYER_QINIUKODO_SECRETKEY="your-secret-key" \ + --CERTIMATE_SSLDEPLOYER_QINIUKODO_DOMAIN="example.com" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEY: %v", fAccessKey), + fmt.Sprintf("SECRETKEY: %v", fSecretKey), + fmt.Sprintf("DOMAIN: %v", fDomain), + }, "\n")) + + deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{ + AccessKey: fAccessKey, + SecretKey: fSecretKey, + Domain: fDomain, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go index 07775b21c..dcbe26b03 100644 --- a/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go +++ b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go @@ -2,9 +2,12 @@ package qiniusslcert import ( "context" + "crypto/x509" "errors" "fmt" "log/slog" + "slices" + "strings" "time" "github.com/qiniu/go-sdk/v7/auth" @@ -24,7 +27,7 @@ type SSLManagerProviderConfig struct { type SSLManagerProvider struct { config *SSLManagerProviderConfig logger *slog.Logger - sdkClient *qiniusdk.CdnManager + sdkClient *qiniusdk.SslCertManager } var _ core.SSLManager = (*SSLManagerProvider)(nil) @@ -64,12 +67,80 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey // 生成新证书名(需符合七牛云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + // 遍历查询已有证书,避免重复上传 + getSslCertListMarker := "" + getSslCertListLimit := int32(200) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + getSslCertListResp, err := m.sdkClient.GetSslCertList(context.TODO(), getSslCertListMarker, getSslCertListLimit) + m.logger.Debug("sdk request 'sslcert.GetList'", slog.Any("request.marker", getSslCertListMarker), slog.Any("response", getSslCertListResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'sslcert.GetList': %w", err) + } + + if getSslCertListResp.Certs != nil { + for _, sslCert := range getSslCertListResp.Certs { + // 先对比证书通用名称 + if !strings.EqualFold(certX509.Subject.CommonName, sslCert.CommonName) { + continue + } + + // 再对比证书多域名 + if !slices.Equal(certX509.DNSNames, sslCert.DnsNames) { + continue + } + + // 再对比证书有效期 + if certX509.NotBefore.Unix() != sslCert.NotBefore || certX509.NotAfter.Unix() != sslCert.NotAfter { + continue + } + + // 最后对比证书公钥算法 + switch certX509.PublicKeyAlgorithm { + case x509.RSA: + if !strings.EqualFold(sslCert.Encrypt, "RSA") { + continue + } + case x509.ECDSA: + if !strings.EqualFold(sslCert.Encrypt, "ECDSA") { + continue + } + case x509.Ed25519: + if !strings.EqualFold(sslCert.Encrypt, "ED25519") { + continue + } + default: + // 未知算法,跳过 + continue + } + + // 如果以上信息都一致,则视为已存在相同证书,直接返回 + m.logger.Info("ssl certificate already exists") + return &core.SSLManageUploadResult{ + CertId: sslCert.CertID, + CertName: sslCert.Name, + }, nil + } + } + + if len(getSslCertListResp.Certs) < int(getSslCertListLimit) || getSslCertListResp.Marker == "" { + break + } else { + getSslCertListMarker = getSslCertListResp.Marker + } + } + // 上传新证书 // REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate uploadSslCertResp, err := m.sdkClient.UploadSslCert(context.TODO(), certName, certX509.Subject.CommonName, certPEM, privkeyPEM) - m.logger.Debug("sdk request 'cdn.UploadSslCert'", slog.Any("response", uploadSslCertResp)) + m.logger.Debug("sdk request 'sslcert.Upload'", slog.Any("response", uploadSslCertResp)) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'cdn.UploadSslCert': %w", err) + return nil, fmt.Errorf("failed to execute sdk request 'sslcert.Upload': %w", err) } return &core.SSLManageUploadResult{ @@ -78,7 +149,7 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey }, nil } -func createSDKClient(accessKey, secretKey string) (*qiniusdk.CdnManager, error) { +func createSDKClient(accessKey, secretKey string) (*qiniusdk.SslCertManager, error) { if secretKey == "" { return nil, errors.New("invalid qiniu access key") } @@ -88,6 +159,6 @@ func createSDKClient(accessKey, secretKey string) (*qiniusdk.CdnManager, error) } credential := auth.New(accessKey, secretKey) - client := qiniusdk.NewCdnManager(credential) + client := qiniusdk.NewSslCertManager(credential) return client, nil } diff --git a/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert_test.go b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert_test.go new file mode 100644 index 000000000..87e14a08a --- /dev/null +++ b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert_test.go @@ -0,0 +1,72 @@ +package qiniusslcert_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/qiniu-sslcert" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKey string + fSecretKey string +) + +func init() { + argsPrefix := "CERTIMATE_SSLMANAGER_QINIUSSLCERT_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") + flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") +} + +/* +Shell command to run this test: + + go test -v ./qiniu_sslcert_test.go -args \ + --CERTIMATE_SSLMANAGER_QINIUSSLCERT_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_SSLMANAGER_QINIUSSLCERT_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_SSLMANAGER_QINIUSSLCERT_ACCESSKEY="your-access-key" \ + --CERTIMATE_SSLMANAGER_QINIUSSLCERT_SECRETKEY="your-secret-key" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEY: %v", fAccessKey), + fmt.Sprintf("SECRETKEY: %v", fSecretKey), + }, "\n")) + + sslmanager, err := provider.NewSSLManagerProvider(&provider.SSLManagerProviderConfig{ + AccessKey: fAccessKey, + SecretKey: fSecretKey, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := sslmanager.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + sres, _ := json.Marshal(res) + t.Logf("ok: %s", string(sres)) + }) +} diff --git a/pkg/sdk3rd/qiniu/cdn.go b/pkg/sdk3rd/qiniu/cdn.go index 54a56517d..74745d0ba 100644 --- a/pkg/sdk3rd/qiniu/cdn.go +++ b/pkg/sdk3rd/qiniu/cdn.go @@ -2,16 +2,12 @@ package qiniu import ( "context" - "fmt" "net/http" - "strings" "github.com/qiniu/go-sdk/v7/auth" "github.com/qiniu/go-sdk/v7/client" ) -const qiniuHost = "https://api.qiniu.com" - type CdnManager struct { client *client.Client } @@ -21,16 +17,10 @@ func NewCdnManager(mac *auth.Credentials) *CdnManager { mac = auth.Default() } - client := &client.Client{&http.Client{Transport: newTransport(mac, nil)}} + client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}} return &CdnManager{client: client} } -func (m *CdnManager) urlf(pathf string, pathargs ...any) string { - path := fmt.Sprintf(pathf, pathargs...) - path = strings.TrimPrefix(path, "/") - return qiniuHost + "/" + path -} - type GetDomainInfoResponse struct { Code *int `json:"code,omitempty"` Error *string `json:"error,omitempty"` @@ -52,7 +42,7 @@ type GetDomainInfoResponse struct { func (m *CdnManager) GetDomainInfo(ctx context.Context, domain string) (*GetDomainInfoResponse, error) { resp := new(GetDomainInfoResponse) - if err := m.client.Call(ctx, resp, http.MethodGet, m.urlf("domain/%s", domain), nil); err != nil { + if err := m.client.Call(ctx, resp, http.MethodGet, urlf("domain/%s", domain), nil); err != nil { return nil, err } return resp, nil @@ -76,7 +66,7 @@ func (m *CdnManager) ModifyDomainHttpsConf(ctx context.Context, domain string, c Http2Enable: http2Enable, } resp := new(ModifyDomainHttpsConfResponse) - if err := m.client.CallWithJson(ctx, resp, http.MethodPut, m.urlf("domain/%s/httpsconf", domain), nil, req); err != nil { + if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("domain/%s/httpsconf", domain), nil, req); err != nil { return nil, err } return resp, nil @@ -100,34 +90,7 @@ func (m *CdnManager) EnableDomainHttps(ctx context.Context, domain string, certI Http2Enable: http2Enable, } resp := new(EnableDomainHttpsResponse) - if err := m.client.CallWithJson(ctx, resp, http.MethodPut, m.urlf("domain/%s/sslize", domain), nil, req); err != nil { - return nil, err - } - return resp, nil -} - -type UploadSslCertRequest struct { - Name string `json:"name"` - CommonName string `json:"common_name"` - Certificate string `json:"ca"` - PrivateKey string `json:"pri"` -} - -type UploadSslCertResponse struct { - Code *int `json:"code,omitempty"` - Error *string `json:"error,omitempty"` - CertID string `json:"certID"` -} - -func (m *CdnManager) UploadSslCert(ctx context.Context, name string, commonName string, certificate string, privateKey string) (*UploadSslCertResponse, error) { - req := &UploadSslCertRequest{ - Name: name, - CommonName: commonName, - Certificate: certificate, - PrivateKey: privateKey, - } - resp := new(UploadSslCertResponse) - if err := m.client.CallWithJson(ctx, resp, http.MethodPost, m.urlf("sslcert"), nil, req); err != nil { + if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("domain/%s/sslize", domain), nil, req); err != nil { return nil, err } return resp, nil diff --git a/pkg/sdk3rd/qiniu/kodo.go b/pkg/sdk3rd/qiniu/kodo.go new file mode 100644 index 000000000..6a3245a28 --- /dev/null +++ b/pkg/sdk3rd/qiniu/kodo.go @@ -0,0 +1,44 @@ +package qiniu + +import ( + "context" + "net/http" + + "github.com/qiniu/go-sdk/v7/auth" + "github.com/qiniu/go-sdk/v7/client" +) + +type KodoManager struct { + client *client.Client +} + +func NewKodoManager(mac *auth.Credentials) *KodoManager { + if mac == nil { + mac = auth.Default() + } + + client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}} + return &KodoManager{client: client} +} + +type BindBucketCertRequest struct { + CertID string `json:"certid"` + Domain string `json:"domain"` +} + +type BindBucketCertResponse struct { + Code *int `json:"code,omitempty"` + Error *string `json:"error,omitempty"` +} + +func (m *KodoManager) BindBucketCert(ctx context.Context, domain string, certId string) (*BindBucketCertResponse, error) { + req := &BindBucketCertRequest{ + CertID: certId, + Domain: domain, + } + resp := new(BindBucketCertResponse) + if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("cert/bind"), nil, req); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/sdk3rd/qiniu/sslcert.go b/pkg/sdk3rd/qiniu/sslcert.go new file mode 100644 index 000000000..f9784270f --- /dev/null +++ b/pkg/sdk3rd/qiniu/sslcert.go @@ -0,0 +1,80 @@ +package qiniu + +import ( + "context" + "net/http" + "net/url" + + "github.com/qiniu/go-sdk/v7/auth" + "github.com/qiniu/go-sdk/v7/client" +) + +type SslCertManager struct { + client *client.Client +} + +func NewSslCertManager(mac *auth.Credentials) *SslCertManager { + if mac == nil { + mac = auth.Default() + } + + client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}} + return &SslCertManager{client: client} +} + +type GetSslCertListResponse struct { + Code *int `json:"code,omitempty"` + Error *string `json:"error,omitempty"` + Certs []*struct { + CertID string `json:"certid"` + Name string `json:"name"` + CommonName string `json:"common_name"` + DnsNames []string `json:"dnsnames"` + CreateTime int64 `json:"create_time"` + NotBefore int64 `json:"not_before"` + NotAfter int64 `json:"not_after"` + ProductType string `json:"product_type"` + ProductShortName string `json:"product_short_name,omitempty"` + OrderId string `json:"orderid,omitempty"` + CertType string `json:"cert_type"` + Encrypt string `json:"encrypt"` + EncryptParameter string `json:"encryptParameter,omitempty"` + Enable bool `json:"enable"` + } `json:"certs"` + Marker string `json:"marker"` +} + +func (m *SslCertManager) GetSslCertList(ctx context.Context, marker string, limit int32) (*GetSslCertListResponse, error) { + resp := new(GetSslCertListResponse) + if err := m.client.Call(ctx, resp, http.MethodGet, urlf("sslcert?marker=%s&limit=%d", url.QueryEscape(marker), limit), nil); err != nil { + return nil, err + } + return resp, nil +} + +type UploadSslCertRequest struct { + Name string `json:"name"` + CommonName string `json:"common_name"` + Certificate string `json:"ca"` + PrivateKey string `json:"pri"` +} + +type UploadSslCertResponse struct { + Code *int `json:"code,omitempty"` + Error *string `json:"error,omitempty"` + CertID string `json:"certID"` +} + +func (m *SslCertManager) UploadSslCert(ctx context.Context, name string, commonName string, certificate string, privateKey string) (*UploadSslCertResponse, error) { + req := &UploadSslCertRequest{ + Name: name, + CommonName: commonName, + Certificate: certificate, + PrivateKey: privateKey, + } + resp := new(UploadSslCertResponse) + if err := m.client.CallWithJson(ctx, resp, http.MethodPost, urlf("sslcert"), nil, req); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/sdk3rd/qiniu/util.go b/pkg/sdk3rd/qiniu/util.go new file mode 100644 index 000000000..0957a3102 --- /dev/null +++ b/pkg/sdk3rd/qiniu/util.go @@ -0,0 +1,14 @@ +package qiniu + +import ( + "fmt" + "strings" +) + +const qiniuHost = "https://api.qiniu.com" + +func urlf(pathf string, pathargs ...any) string { + path := fmt.Sprintf(pathf, pathargs...) + path = strings.TrimPrefix(path, "/") + return qiniuHost + "/" + path +} diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index df7f19152..bc5a16a1b 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -268,8 +268,8 @@ "workflow_node.deploy.form.aliyun_oss_bucket.label": "Alibaba Cloud OSS bucket", "workflow_node.deploy.form.aliyun_oss_bucket.placeholder": "Please enter Alibaba Cloud OSS bucket name", "workflow_node.deploy.form.aliyun_oss_bucket.tooltip": "For more information, see https://oss.console.aliyun.com", - "workflow_node.deploy.form.aliyun_oss_domain.label": "Alibaba Cloud OSS domain", - "workflow_node.deploy.form.aliyun_oss_domain.placeholder": "Please enter Alibaba Cloud OSS domain name", + "workflow_node.deploy.form.aliyun_oss_domain.label": "Alibaba Cloud OSS custom domain", + "workflow_node.deploy.form.aliyun_oss_domain.placeholder": "Please enter Alibaba Cloud OSS bucket custom domain name", "workflow_node.deploy.form.aliyun_oss_domain.tooltip": "For more information, see https://oss.console.aliyun.com", "workflow_node.deploy.form.aliyun_vod_region.label": "Alibaba Cloud VOD region", "workflow_node.deploy.form.aliyun_vod_region.placeholder": "Please enter Alibaba Cloud VOD region (e.g. cn-hangzhou)", @@ -601,8 +601,8 @@ "workflow_node.deploy.form.qiniu_cdn_domain.label": "Qiniu CDN domain", "workflow_node.deploy.form.qiniu_cdn_domain.placeholder": "Please enter Qiniu CDN domain name", "workflow_node.deploy.form.qiniu_cdn_domain.tooltip": "For more information, see https://portal.qiniu.com/cdn", - "workflow_node.deploy.form.qiniu_kodo_domain.label": "Qiniu Kodo bucket domain", - "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "Please enter Qiniu Kodo bucket domain name", + "workflow_node.deploy.form.qiniu_kodo_domain.label": "Qiniu Kodo custom domain", + "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "Please enter Qiniu Kodo bucket custom domain name", "workflow_node.deploy.form.qiniu_kodo_domain.tooltip": "For more information, see https://portal.qiniu.com/kodo", "workflow_node.deploy.form.qiniu_pili_hub.label": "Qiniu Pili hub", "workflow_node.deploy.form.qiniu_pili_hub.placeholder": "Please enter Qiniu Pili hub name", @@ -706,8 +706,8 @@ "workflow_node.deploy.form.tencentcloud_cos_bucket.label": "Tencent Cloud COS bucket", "workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder": "Please enter Tencent Cloud COS bucket name", "workflow_node.deploy.form.tencentcloud_cos_bucket.tooltip": "For more information, see https://console.tencentcloud.com/cos", - "workflow_node.deploy.form.tencentcloud_cos_domain.label": "Tencent Cloud COS domain", - "workflow_node.deploy.form.tencentcloud_cos_domain.placeholder": "Please enter Tencent Cloud COS domain name", + "workflow_node.deploy.form.tencentcloud_cos_domain.label": "Tencent Cloud COS custom domain", + "workflow_node.deploy.form.tencentcloud_cos_domain.placeholder": "Please enter Tencent Cloud COS bucket custom domain name", "workflow_node.deploy.form.tencentcloud_cos_domain.tooltip": "For more information, see https://console.tencentcloud.com/cos", "workflow_node.deploy.form.tencentcloud_css_endpoint.label": "Tencent Cloud CSS API endpoint (Optional)", "workflow_node.deploy.form.tencentcloud_css_endpoint.placeholder": "Please enter Tencent Cloud CSS API endpoint (e.g. live.intl.tencentcloudapi.com)", @@ -824,8 +824,8 @@ "workflow_node.deploy.form.ucloud_us3_bucket.label": "UCloud US3 bucket", "workflow_node.deploy.form.ucloud_us3_bucket.placeholder": "Please enter UCloud US3 bucket name", "workflow_node.deploy.form.ucloud_us3_bucket.tooltip": "For more information, see https://console.ucloud-global.com/ufile", - "workflow_node.deploy.form.ucloud_us3_domain.label": "UCloud US3 domain", - "workflow_node.deploy.form.ucloud_us3_domain.placeholder": "Please enter UCloud US3 domain name", + "workflow_node.deploy.form.ucloud_us3_domain.label": "UCloud US3 custom domain", + "workflow_node.deploy.form.ucloud_us3_domain.placeholder": "Please enter UCloud US3 bucket custom domain name", "workflow_node.deploy.form.ucloud_us3_domain.tooltip": "For more information, see https://console.ucloud-global.com/ufile", "workflow_node.deploy.form.unicloud_webhost.guide": "Tips: This uses webpage simulator login and does not guarantee stability. If there are any changes to the uniCloud, please create a GitHub Issue.", "workflow_node.deploy.form.unicloud_webhost_space_provider.label": "uniCloud space provider", @@ -842,8 +842,8 @@ "workflow_node.deploy.form.upyun_cdn_domain.placeholder": "Please enter UPYUN CDN domain name", "workflow_node.deploy.form.upyun_cdn_domain.tooltip": "For more information, see https://console.upyun.com/services/cdn/", "workflow_node.deploy.form.upyun_file.guide": "Tips: This uses webpage simulator login and does not guarantee stability. If there are any changes to the UPYUN, please create a GitHub Issue.", - "workflow_node.deploy.form.upyun_file_domain.label": "UPYUN bucket domain", - "workflow_node.deploy.form.upyun_file_domain.placeholder": "Please enter UPYUN bucket domain name", + "workflow_node.deploy.form.upyun_file_domain.label": "UPYUN USS custom domain", + "workflow_node.deploy.form.upyun_file_domain.placeholder": "Please enter UPYUN USS bucket custom domain name", "workflow_node.deploy.form.upyun_file_domain.tooltip": "For more information, see https://console.upyun.com/services/file/", "workflow_node.deploy.form.volcengine_alb_region.label": "VolcEngine ALB region", "workflow_node.deploy.form.volcengine_alb_region.placeholder": "Please enter VolcEngine ALB region (e.g. cn-beijing)", @@ -888,8 +888,8 @@ "workflow_node.deploy.form.volcengine_imagex_service_id.label": "VolcEngine ImageX service ID", "workflow_node.deploy.form.volcengine_imagex_service_id.placeholder": "Please enter VolcEngine ImageX service ID", "workflow_node.deploy.form.volcengine_imagex_service_id.tooltip": "For more information, see https://console.volcengine.com/imagex", - "workflow_node.deploy.form.volcengine_imagex_domain.label": "VolcEngine ImageX domain", - "workflow_node.deploy.form.volcengine_imagex_domain.placeholder": "Please enter VolcEngine ImageX domain name", + "workflow_node.deploy.form.volcengine_imagex_domain.label": "VolcEngine ImageX custom domain", + "workflow_node.deploy.form.volcengine_imagex_domain.placeholder": "Please enter VolcEngine ImageX custom domain name", "workflow_node.deploy.form.volcengine_imagex_domain.tooltip": "For more information, see https://console.volcengine.com/imagex", "workflow_node.deploy.form.volcengine_live_domain.label": "VolcEngine Live streaming domain", "workflow_node.deploy.form.volcengine_live_domain.placeholder": "Please enter VolcEngine Live streaming domain name", @@ -900,8 +900,8 @@ "workflow_node.deploy.form.volcengine_tos_bucket.label": "VolcEngine TOS bucket", "workflow_node.deploy.form.volcengine_tos_bucket.placeholder": "Please enter VolcEngine TOS bucket name", "workflow_node.deploy.form.volcengine_tos_bucket.tooltip": "For more information, see https://console.volcengine.com/tos", - "workflow_node.deploy.form.volcengine_tos_domain.label": "VolcEngine TOS domain", - "workflow_node.deploy.form.volcengine_tos_domain.placeholder": "Please enter VolcEngine TOS domain name", + "workflow_node.deploy.form.volcengine_tos_domain.label": "VolcEngine TOS custom domain", + "workflow_node.deploy.form.volcengine_tos_domain.placeholder": "Please enter VolcEngine TOS bucket custom domain name", "workflow_node.deploy.form.volcengine_tos_domain.tooltip": "For more information, see https://console.volcengine.com/tos", "workflow_node.deploy.form.wangsu_cdn_domains.label": "Wangsu Cloud CDN domains", "workflow_node.deploy.form.wangsu_cdn_domains.placeholder": "Please enter Wangsu Cloud CDN domain names (separated by semicolons)", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 061189dfd..09fc27e95 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -599,8 +599,8 @@ "workflow_node.deploy.form.qiniu_cdn_domain.label": "七牛云 CDN 加速域名", "workflow_node.deploy.form.qiniu_cdn_domain.placeholder": "请输入七牛云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.qiniu_cdn_domain.tooltip": "这是什么?请参阅 https://portal.qiniu.com/cdn", - "workflow_node.deploy.form.qiniu_kodo_domain.label": "七牛云对象存储加速域名", - "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "请输入七牛云对象存储加速域名", + "workflow_node.deploy.form.qiniu_kodo_domain.label": "七牛云对象存储自定义域名", + "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "请输入七牛云对象存储自定义域名", "workflow_node.deploy.form.qiniu_kodo_domain.tooltip": "这是什么?请参阅 https://portal.qiniu.com/kodo", "workflow_node.deploy.form.qiniu_pili_hub.label": "七牛云视频直播空间名", "workflow_node.deploy.form.qiniu_pili_hub.placeholder": "请输入七牛云视频直播空间名", @@ -840,8 +840,8 @@ "workflow_node.deploy.form.upyun_cdn_domain.placeholder": "请输入又拍云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.upyun_cdn_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/cdn/", "workflow_node.deploy.form.upyun_file.guide": "小贴士:由于又拍云未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", - "workflow_node.deploy.form.upyun_file_domain.label": "又拍云云存储加速域名", - "workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储加速域名", + "workflow_node.deploy.form.upyun_file_domain.label": "又拍云云存储自定义域名", + "workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储自定义域名", "workflow_node.deploy.form.upyun_file_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/file/", "workflow_node.deploy.form.volcengine_alb_resource_type.label": "证书部署方式", "workflow_node.deploy.form.volcengine_alb_resource_type.placeholder": "请选择证书部署方式", From 07d5c2051cceb65e38f931de98f1fb123561f8ac Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 8 Jul 2025 10:37:44 +0800 Subject: [PATCH 8/8] fix: missing valid from field --- .../workflow/node/DeployNodeConfigFormSSHConfig.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx index ebeb1cff9..132de43f0 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx @@ -152,9 +152,11 @@ chmod 755 "$fnCertPath" chmod 755 "$fnKeyPath" # 更新数据库 +NEW_EFFECT_DATE=$(openssl x509 -startdate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/") +NEW_EFFECT_TIMESTAMP=$(date -d "$NEW_EFFECT_DATE" +%s%3N) NEW_EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/") NEW_EXPIRY_TIMESTAMP=$(date -d "$NEW_EXPIRY_DATE" +%s%3N) -psql -U postgres -d trim_connect -c "UPDATE cert SET valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'" +psql -U postgres -d trim_connect -c "UPDATE cert SET valid_from=$NEW_EFFECT_TIMESTAMP, valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'" # 重启服务 systemctl restart webdav.service