From 12c208cad44566cc689fd7936f4a61a46bec7675 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 7 May 2025 15:17:25 +0800 Subject: [PATCH 1/7] feat: new deployment provider: aliyun ddoscoo --- go.mod | 1 + go.sum | 2 + internal/deployer/providers.go | 12 +- internal/domain/provider.go | 1 + .../providers/aliyun-alb/aliyun_alb.go | 2 +- .../providers/aliyun-apigw/aliyun_apigw.go | 2 +- .../providers/aliyun-clb/aliyun_clb.go | 28 +++- .../providers/aliyun-ddos/aliyun_ddos.go | 137 ++++++++++++++++++ .../providers/aliyun-ddos/aliyun_ddos_test.go | 80 ++++++++++ .../providers/aliyun-esa/aliyun_esa.go | 2 +- .../providers/aliyun-nlb/aliyun_nlb.go | 2 +- .../providers/aliyun-waf/aliyun_waf.go | 2 +- .../workflow/node/DeployNodeConfigForm.tsx | 3 + .../DeployNodeConfigFormAliyunDDoSConfig.tsx | 79 ++++++++++ ui/src/domain/provider.ts | 2 + ui/src/i18n/locales/en/nls.provider.json | 1 + .../i18n/locales/en/nls.workflow.nodes.json | 6 + ui/src/i18n/locales/zh/nls.provider.json | 3 +- .../i18n/locales/zh/nls.workflow.nodes.json | 6 + 19 files changed, 359 insertions(+), 12 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go create mode 100644 internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos_test.go create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormAliyunDDoSConfig.tsx diff --git a/go.mod b/go.mod index 54651504..06ea5977 100644 --- a/go.mod +++ b/go.mod @@ -75,6 +75,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect github.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 // indirect + github.com/alibabacloud-go/ddoscoo-20200101/v4 v4.0.0 // indirect github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect diff --git a/go.sum b/go.sum index 89b1fec2..b1370567 100644 --- a/go.sum +++ b/go.sum @@ -122,6 +122,8 @@ github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5 github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= github.com/alibabacloud-go/dcdn-20180115/v3 v3.5.0 h1:EQmKhYju6y38kJ1ZvZROeJG2Q1Wk6hlc8KQrVhvGyaw= github.com/alibabacloud-go/dcdn-20180115/v3 v3.5.0/go.mod h1:b9qzvr/2V1f0r1Z6xUmkLqEouKcPGy4LCC22yV+6HQo= +github.com/alibabacloud-go/ddoscoo-20200101/v4 v4.0.0 h1:z9dPOvBRxzpD+FQ2uu/p2Z92I+PY9MUZMauwC+8AC6M= +github.com/alibabacloud-go/ddoscoo-20200101/v4 v4.0.0/go.mod h1:Cdg3Fu4jFByamRzt3AkeiBssoVPRNDs+EPYMP2fIj78= github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 2e7bcbcf..9dd36e32 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -16,6 +16,7 @@ import ( pAliyunCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-cdn" pAliyunCLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-clb" pAliyunDCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-dcdn" + pAliyunDDoS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-ddos" pAliyunESA "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-esa" pAliyunFC "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-fc" pAliyunLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-live" @@ -129,7 +130,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer } } - case domain.DeploymentProviderTypeAliyunALB, domain.DeploymentProviderTypeAliyunAPIGW, domain.DeploymentProviderTypeAliyunCAS, domain.DeploymentProviderTypeAliyunCASDeploy, domain.DeploymentProviderTypeAliyunCDN, domain.DeploymentProviderTypeAliyunCLB, domain.DeploymentProviderTypeAliyunDCDN, domain.DeploymentProviderTypeAliyunESA, domain.DeploymentProviderTypeAliyunFC, domain.DeploymentProviderTypeAliyunLive, domain.DeploymentProviderTypeAliyunNLB, domain.DeploymentProviderTypeAliyunOSS, domain.DeploymentProviderTypeAliyunVOD, domain.DeploymentProviderTypeAliyunWAF: + case domain.DeploymentProviderTypeAliyunALB, domain.DeploymentProviderTypeAliyunAPIGW, domain.DeploymentProviderTypeAliyunCAS, domain.DeploymentProviderTypeAliyunCASDeploy, domain.DeploymentProviderTypeAliyunCDN, domain.DeploymentProviderTypeAliyunCLB, domain.DeploymentProviderTypeAliyunDCDN, domain.DeploymentProviderTypeAliyunDDoS, domain.DeploymentProviderTypeAliyunESA, domain.DeploymentProviderTypeAliyunFC, domain.DeploymentProviderTypeAliyunLive, domain.DeploymentProviderTypeAliyunNLB, domain.DeploymentProviderTypeAliyunOSS, domain.DeploymentProviderTypeAliyunVOD, domain.DeploymentProviderTypeAliyunWAF: { access := domain.AccessConfigForAliyun{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -207,6 +208,15 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer }) return deployer, err + case domain.DeploymentProviderTypeAliyunDDoS: + deployer, err := pAliyunDDoS.NewDeployer(&pAliyunDDoS.DeployerConfig{ + AccessKeyId: access.AccessKeyId, + AccessKeySecret: access.AccessKeySecret, + Region: maputil.GetString(options.ProviderExtendedConfig, "region"), + Domain: maputil.GetString(options.ProviderExtendedConfig, "domain"), + }) + return deployer, err + case domain.DeploymentProviderTypeAliyunESA: deployer, err := pAliyunESA.NewDeployer(&pAliyunESA.DeployerConfig{ AccessKeyId: access.AccessKeyId, diff --git a/internal/domain/provider.go b/internal/domain/provider.go index cb2ae3c9..a99b8717 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -160,6 +160,7 @@ const ( DeploymentProviderTypeAliyunCDN = DeploymentProviderType(AccessProviderTypeAliyun + "-cdn") DeploymentProviderTypeAliyunCLB = DeploymentProviderType(AccessProviderTypeAliyun + "-clb") DeploymentProviderTypeAliyunDCDN = DeploymentProviderType(AccessProviderTypeAliyun + "-dcdn") + DeploymentProviderTypeAliyunDDoS = DeploymentProviderType(AccessProviderTypeAliyun + "-ddos") DeploymentProviderTypeAliyunESA = DeploymentProviderType(AccessProviderTypeAliyun + "-esa") DeploymentProviderTypeAliyunFC = DeploymentProviderType(AccessProviderTypeAliyun + "-fc") DeploymentProviderTypeAliyunLive = DeploymentProviderType(AccessProviderTypeAliyun + "-live") diff --git a/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go b/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go index 1d60d315..3dca4a9d 100644 --- a/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go +++ b/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go @@ -464,7 +464,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up // 阿里云 CAS 服务接入点是独立于 ALB 服务的 // 国内版固定接入点:华东一杭州 // 国际版固定接入点:亚太东南一新加坡 - if casRegion != "" && !strings.HasPrefix(casRegion, "cn-") { + if !strings.HasPrefix(casRegion, "cn-") { casRegion = "ap-southeast-1" } else { casRegion = "cn-hangzhou" diff --git a/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go b/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go index 82a05c33..d74c7c27 100644 --- a/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go +++ b/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go @@ -258,7 +258,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up // 阿里云 CAS 服务接入点是独立于 APIGateway 服务的 // 国内版固定接入点:华东一杭州 // 国际版固定接入点:亚太东南一新加坡 - if casRegion != "" && !strings.HasPrefix(casRegion, "cn-") { + if !strings.HasPrefix(casRegion, "cn-") { casRegion = "ap-southeast-1" } else { casRegion = "cn-hangzhou" diff --git a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go index 6ff33049..41a78968 100644 --- a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go +++ b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log/slog" + "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" alislb "github.com/alibabacloud-go/slb-20140515/v4/client" @@ -54,11 +55,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - AccessKeyId: config.AccessKeyId, - AccessKeySecret: config.AccessKeySecret, - Region: config.Region, - }) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -311,3 +308,24 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alislb.Clien return client, nil } + +func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { + casRegion := region + if casRegion != "" { + // 阿里云 CAS 服务接入点是独立于 CLB 服务的 + // 国内版固定接入点:华东一杭州 + // 国际版固定接入点:亚太东南一新加坡 + if !strings.HasPrefix(casRegion, "cn-") { + casRegion = "ap-southeast-1" + } else { + casRegion = "cn-hangzhou" + } + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: accessKeyId, + AccessKeySecret: accessKeySecret, + Region: casRegion, + }) + return uploader, err +} diff --git a/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go new file mode 100644 index 00000000..d1cb5b61 --- /dev/null +++ b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go @@ -0,0 +1,137 @@ +package aliyunddos + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strconv" + "strings" + + aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" + aliddos "github.com/alibabacloud-go/ddoscoo-20200101/v4/client" + "github.com/alibabacloud-go/tea/tea" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-slb" +) + +type DeployerConfig struct { + // 阿里云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 阿里云 AccessKeySecret。 + AccessKeySecret string `json:"accessKeySecret"` + // 阿里云地域。 + Region string `json:"region"` + // 网站域名(支持泛域名)。 + Domain string `json:"domain"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *aliddos.Client + sslUploader uploader.Uploader +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region) + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + if err != nil { + return nil, fmt.Errorf("failed to create ssl uploader: %w", err) + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + sslUploader: uploader, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + d.sslUploader.WithLogger(logger) + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { + if d.config.Domain == "" { + return nil, errors.New("config `domain` is required") + } + + // 上传证书到 CAS + upres, err := d.sslUploader.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)) + } + + // 为网站业务转发规则关联 SSL 证书 + // REF: https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-associatewebcert + certId, _ := strconv.Atoi(upres.CertId) + associateWebCertReq := &aliddos.AssociateWebCertRequest{ + Domain: tea.String(d.config.Domain), + CertId: tea.Int32(int32(certId)), + } + associateWebCertResp, err := d.sdkClient.AssociateWebCert(associateWebCertReq) + d.logger.Debug("sdk request 'dcdn.AssociateWebCert'", slog.Any("request", associateWebCertReq), slog.Any("response", associateWebCertResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'dcdn.AssociateWebCert': %w", err) + } + + return &deployer.DeployResult{}, nil +} + +func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliddos.Client, error) { + // 接入点一览 https://api.aliyun.com/product/ddoscoo + config := &aliopen.Config{ + AccessKeyId: tea.String(accessKeyId), + AccessKeySecret: tea.String(accessKeySecret), + Endpoint: tea.String(fmt.Sprintf("ddoscoo.%s.aliyuncs.com", region)), + } + + client, err := aliddos.NewClient(config) + if err != nil { + return nil, err + } + + return client, nil +} + +func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { + casRegion := region + if casRegion != "" { + // 阿里云 CAS 服务接入点是独立于 Anti-DDoS 服务的 + // 国内版固定接入点:华东一杭州 + // 国际版固定接入点:亚太东南一新加坡 + if !strings.HasPrefix(casRegion, "cn-") { + casRegion = "ap-southeast-1" + } else { + casRegion = "cn-hangzhou" + } + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: accessKeyId, + AccessKeySecret: accessKeySecret, + Region: casRegion, + }) + return uploader, err +} diff --git a/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos_test.go b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos_test.go new file mode 100644 index 00000000..b66f924f --- /dev/null +++ b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos_test.go @@ -0,0 +1,80 @@ +package aliyunddos_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-ddos" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fAccessKeySecret string + fRegion string + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNDCDN_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") + flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") +} + +/* +Shell command to run this test: + + go test -v ./aliyun_ddos_test.go -args \ + --CERTIMATE_DEPLOYER_ALIYUNDDOS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_ALIYUNDDOS_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_ALIYUNDDOS_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_ALIYUNDDOS_ACCESSKEYSECRET="your-access-key-secret" \ + --CERTIMATE_DEPLOYER_ALIYUNDDOS_REGION="cn-hangzhou" \ + --CERTIMATE_DEPLOYER_ALIYUNDDOS_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("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), + fmt.Sprintf("REGION: %v", fRegion), + fmt.Sprintf("DOMAIN: %v", fDomain), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + AccessKeyId: fAccessKeyId, + AccessKeySecret: fAccessKeySecret, + Region: fRegion, + 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/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go b/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go index 6052eee2..1f29756f 100644 --- a/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go +++ b/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go @@ -122,7 +122,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up // 阿里云 CAS 服务接入点是独立于 ESA 服务的 // 国内版固定接入点:华东一杭州 // 国际版固定接入点:亚太东南一新加坡 - if casRegion != "" && !strings.HasPrefix(casRegion, "cn-") { + if !strings.HasPrefix(casRegion, "cn-") { casRegion = "ap-southeast-1" } else { casRegion = "cn-hangzhou" diff --git a/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go b/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go index d6879f61..b8391144 100644 --- a/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go +++ b/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go @@ -251,7 +251,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up // 阿里云 CAS 服务接入点是独立于 NLB 服务的 // 国内版固定接入点:华东一杭州 // 国际版固定接入点:亚太东南一新加坡 - if casRegion != "" && !strings.HasPrefix(casRegion, "cn-") { + if !strings.HasPrefix(casRegion, "cn-") { casRegion = "ap-southeast-1" } else { casRegion = "cn-hangzhou" diff --git a/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go b/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go index dfe2f27c..26dbd008 100644 --- a/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go +++ b/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go @@ -192,7 +192,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up // 阿里云 CAS 服务接入点是独立于 WAF 服务的 // 国内版固定接入点:华东一杭州 // 国际版固定接入点:亚太东南一新加坡 - if casRegion != "" && !strings.HasPrefix(casRegion, "cn-") { + if !strings.HasPrefix(casRegion, "cn-") { casRegion = "ap-southeast-1" } else { casRegion = "cn-hangzhou" diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 6a3a944e..a9c83a65 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -24,6 +24,7 @@ import DeployNodeConfigFormAliyunCASDeployConfig from "./DeployNodeConfigFormAli import DeployNodeConfigFormAliyunCDNConfig from "./DeployNodeConfigFormAliyunCDNConfig"; import DeployNodeConfigFormAliyunCLBConfig from "./DeployNodeConfigFormAliyunCLBConfig"; import DeployNodeConfigFormAliyunDCDNConfig from "./DeployNodeConfigFormAliyunDCDNConfig"; +import DeployNodeConfigFormAliyunDDoSConfig from "./DeployNodeConfigFormAliyunDDoSConfig"; import DeployNodeConfigFormAliyunESAConfig from "./DeployNodeConfigFormAliyunESAConfig"; import DeployNodeConfigFormAliyunFCConfig from "./DeployNodeConfigFormAliyunFCConfig"; import DeployNodeConfigFormAliyunLiveConfig from "./DeployNodeConfigFormAliyunLiveConfig"; @@ -191,6 +192,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOYMENT_PROVIDERS.ALIYUN_DCDN: return ; + case DEPLOYMENT_PROVIDERS.ALIYUN_DDOS: + return ; case DEPLOYMENT_PROVIDERS.ALIYUN_ESA: return ; case DEPLOYMENT_PROVIDERS.ALIYUN_FC: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunDDoSConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunDDoSConfig.tsx new file mode 100644 index 00000000..f8887794 --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunDDoSConfig.tsx @@ -0,0 +1,79 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { validDomainName } from "@/utils/validators"; + +type DeployNodeConfigFormAliyunDDoSConfigFieldValues = Nullish<{ + region: string; + domain: string; +}>; + +export type DeployNodeConfigFormAliyunDDoSConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormAliyunDDoSConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormAliyunDDoSConfigFieldValues) => void; +}; + +const initFormModel = (): DeployNodeConfigFormAliyunDDoSConfigFieldValues => { + return {}; +}; + +const DeployNodeConfigFormAliyunDDoSConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormAliyunDDoSConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + region: z + .string({ message: t("workflow_node.deploy.form.aliyun_ddos_region.placeholder") }) + .nonempty(t("workflow_node.deploy.form.aliyun_ddos_region.placeholder")) + .trim(), + domain: z + .string({ message: t("workflow_node.deploy.form.aliyun_ddos_domain.placeholder") }) + .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default DeployNodeConfigFormAliyunDDoSConfig; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 73cc7e7d..642a42da 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -328,6 +328,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({ ALIYUN_CDN: `${ACCESS_PROVIDERS.ALIYUN}-cdn`, ALIYUN_CLB: `${ACCESS_PROVIDERS.ALIYUN}-clb`, ALIYUN_DCDN: `${ACCESS_PROVIDERS.ALIYUN}-dcdn`, + ALIYUN_DDOS: `${ACCESS_PROVIDERS.ALIYUN}-ddospro`, ALIYUN_ESA: `${ACCESS_PROVIDERS.ALIYUN}-esa`, ALIYUN_FC: `${ACCESS_PROVIDERS.ALIYUN}-fc`, ALIYUN_LIVE: `${ACCESS_PROVIDERS.ALIYUN}-live`, @@ -438,6 +439,7 @@ export const deploymentProvidersMap: Maphttps://dcdn.console.aliyun.com", + "workflow_node.deploy.form.aliyun_ddos_region.label": "Alibaba Cloud Anti-DDoS region", + "workflow_node.deploy.form.aliyun_ddos_region.placeholder": "Please enter Alibaba Cloud Anti-DDoS region (e.g. cn-hangzhou)", + "workflow_node.deploy.form.aliyun_ddos_region.tooltip": "For more information, see https://www.alibabacloud.com/help/en/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-endpoint", + "workflow_node.deploy.form.aliyun_ddos_domain.label": "Alibaba Cloud Anti-DDoS domain", + "workflow_node.deploy.form.aliyun_ddos_domain.placeholder": "Please enter Alibaba Cloud Anti-DDoS domain name", + "workflow_node.deploy.form.aliyun_ddos_domain.tooltip": "For more information, see https://yundun.console.aliyun.com/?p=ddoscoo", "workflow_node.deploy.form.aliyun_esa_region.label": "Alibaba Cloud ESA region", "workflow_node.deploy.form.aliyun_esa_region.placeholder": "Please enter Alibaba Cloud ESA region (e.g. cn-hangzhou)", "workflow_node.deploy.form.aliyun_esa_region.tooltip": "For more information, see https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 28c3ad82..00e45abf 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -11,9 +11,10 @@ "provider.aliyun.cdn": "阿里云 - 内容分发网络 CDN", "provider.aliyun.clb": "阿里云 - 传统型负载均衡 CLB", "provider.aliyun.dcdn": "阿里云 - 全站加速 DCDN", + "provider.aliyun.ddos": "阿里云 - DDoS 高防", + "provider.aliyun.dns": "阿里云 - 云解析 DNS", "provider.aliyun.esa": "阿里云 - 边缘安全加速 ESA", "provider.aliyun.fc": "阿里云 - 函数计算 FC", - "provider.aliyun.dns": "阿里云 - 云解析 DNS", "provider.aliyun.live": "阿里云 - 视频直播 Live", "provider.aliyun.nlb": "阿里云 - 网络型负载均衡 NLB", "provider.aliyun.oss": "阿里云 - 对象存储 OSS", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index d58ebe90..44cf3024 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -185,6 +185,12 @@ "workflow_node.deploy.form.aliyun_dcdn_domain.label": "阿里云 DCDN 加速域名", "workflow_node.deploy.form.aliyun_dcdn_domain.placeholder": "请输入阿里云 DCDN 加速域名(支持泛域名)", "workflow_node.deploy.form.aliyun_dcdn_domain.tooltip": "这是什么?请参阅 https://dcdn.console.aliyun.com", + "workflow_node.deploy.form.aliyun_ddos_region.label": "阿里云 DDoS 高防服务地域", + "workflow_node.deploy.form.aliyun_ddos_region.placeholder": "请输入阿里云 DDoS 高防服务地域(例如:cn-hangzhou)", + "workflow_node.deploy.form.aliyun_ddos_region.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-endpoint", + "workflow_node.deploy.form.aliyun_ddos_domain.label": "阿里云 DDoS 高防网站域名", + "workflow_node.deploy.form.aliyun_ddos_domain.placeholder": "请输入阿里云 DDoS 高防网站域名(支持泛域名)", + "workflow_node.deploy.form.aliyun_ddos_domain.tooltip": "这是什么?请参阅 https://yundun.console.aliyun.com/?p=ddoscoo", "workflow_node.deploy.form.aliyun_esa_region.label": "阿里云 ESA 服务地域", "workflow_node.deploy.form.aliyun_esa_region.placeholder": "请输入阿里云 ESA 服务地域(例如:cn-hangzhou)", "workflow_node.deploy.form.aliyun_esa_region.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint", From 5cb0463cf66d95492c2f939cfeed276ac3f1b9dd Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 7 May 2025 15:30:25 +0800 Subject: [PATCH 2/7] feat: set the default max workers to the number of available CPU cores --- internal/workflow/dispatcher/dispatcher.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/workflow/dispatcher/dispatcher.go b/internal/workflow/dispatcher/dispatcher.go index 33aa1e0d..f25fadcf 100644 --- a/internal/workflow/dispatcher/dispatcher.go +++ b/internal/workflow/dispatcher/dispatcher.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "runtime" "strconv" "sync" "time" @@ -14,7 +15,7 @@ import ( sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice" ) -var maxWorkers = 16 +var maxWorkers = runtime.NumCPU() func init() { envMaxWorkers := os.Getenv("CERTIMATE_WORKFLOW_MAX_WORKERS") From e5805a028b6be0cc46ff418f01fc50c929af1a6a Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 7 May 2025 19:54:04 +0800 Subject: [PATCH 3/7] feat: new acme dns-01 provider: aliyun esa --- internal/applicant/providers.go | 33 ++- internal/domain/provider.go | 1 + .../lego-providers/aliyun-esa/aliyun_esa.go | 40 +++ .../aliyun-esa/internal/lego.go | 266 ++++++++++++++++++ .../baiducloud/internal/lego.go | 6 +- .../lego-providers/dynv6/internal/lego.go | 6 +- .../lego-providers/gname/internal/lego.go | 6 +- .../tencentcloud-eo/internal/lego.go | 27 +- .../tencentcloud-eo/tencentcloud_eo.go | 2 +- .../workflow/node/ApplyNodeConfigForm.tsx | 5 +- .../ApplyNodeConfigFormAliyunESAConfig.tsx | 58 ++++ ui/src/domain/provider.ts | 2 + .../i18n/locales/en/nls.workflow.nodes.json | 3 + .../i18n/locales/zh/nls.workflow.nodes.json | 3 + 14 files changed, 426 insertions(+), 32 deletions(-) create mode 100644 internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa/aliyun_esa.go create mode 100644 internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa/internal/lego.go create mode 100644 ui/src/components/workflow/node/ApplyNodeConfigFormAliyunESAConfig.tsx diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index c54d1fe2..8f9fe89e 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -8,6 +8,7 @@ import ( "github.com/usual2970/certimate/internal/domain" pACMEHttpReq "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/acmehttpreq" pAliyun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun" + pAliyunESA "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa" pAWSRoute53 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/aws-route53" pAzureDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/azure-dns" pBaiduCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud" @@ -79,20 +80,36 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi return applicant, err } - case domain.ACMEDns01ProviderTypeAliyun, domain.ACMEDns01ProviderTypeAliyunDNS: + case domain.ACMEDns01ProviderTypeAliyun, domain.ACMEDns01ProviderTypeAliyunDNS, domain.ACMEDns01ProviderTypeAliyunESA: { access := domain.AccessConfigForAliyun{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } - applicant, err := pAliyun.NewChallengeProvider(&pAliyun.ChallengeProviderConfig{ - AccessKeyId: access.AccessKeyId, - AccessKeySecret: access.AccessKeySecret, - DnsPropagationTimeout: options.DnsPropagationTimeout, - DnsTTL: options.DnsTTL, - }) - return applicant, err + switch options.Provider { + case domain.ACMEDns01ProviderTypeAliyun, domain.ACMEDns01ProviderTypeAliyunDNS: + applicant, err := pAliyun.NewChallengeProvider(&pAliyun.ChallengeProviderConfig{ + AccessKeyId: access.AccessKeyId, + AccessKeySecret: access.AccessKeySecret, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + + case domain.ACMEDns01ProviderTypeAliyunESA: + applicant, err := pAliyunESA.NewChallengeProvider(&pAliyunESA.ChallengeProviderConfig{ + AccessKeyId: access.AccessKeyId, + AccessKeySecret: access.AccessKeySecret, + Region: maputil.GetString(options.ProviderExtendedConfig, "region"), + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + + default: + break + } } case domain.ACMEDns01ProviderTypeAWS, domain.ACMEDns01ProviderTypeAWSRoute53: diff --git a/internal/domain/provider.go b/internal/domain/provider.go index a99b8717..61af784f 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -105,6 +105,7 @@ const ( ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq) ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS] ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns") + ACMEDns01ProviderTypeAliyunESA = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-esa") ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53] ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53") ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure] diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa/aliyun_esa.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa/aliyun_esa.go new file mode 100644 index 00000000..bf7026da --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa/aliyun_esa.go @@ -0,0 +1,40 @@ +package aliyunesa + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + + internal "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa/internal" +) + +type ChallengeProviderConfig struct { + AccessKeyId string `json:"accessKeyId"` + AccessKeySecret string `json:"accessKeySecret"` + Region string `json:"region"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) { + if config == nil { + panic("config is nil") + } + + providerConfig := internal.NewDefaultConfig() + providerConfig.SecretID = config.AccessKeyId + providerConfig.SecretKey = config.AccessKeySecret + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = config.DnsTTL + } + + provider, err := internal.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa/internal/lego.go new file mode 100644 index 00000000..79df4083 --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/aliyun-esa/internal/lego.go @@ -0,0 +1,266 @@ +package lego_aliyunesa + +import ( + "errors" + "fmt" + "strings" + "sync" + "time" + + aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" + aliesa "github.com/alibabacloud-go/esa-20240910/v2/client" + "github.com/alibabacloud-go/tea/tea" + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" +) + +const ( + envNamespace = "ALICLOUDESA_" + + EnvAccessKey = envNamespace + "ACCESS_KEY" + EnvSecretKey = envNamespace + "SECRET_KEY" + EnvRegionID = envNamespace + "REGION_ID" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +type Config struct { + SecretID string + SecretKey string + RegionID string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int32 + HTTPTimeout time.Duration +} + +type DNSProvider struct { + client *aliesa.Client + config *Config + + siteIDs map[string]int64 + siteIDsMtx sync.Mutex +} + +func NewDefaultConfig() *Config { + return &Config{ + TTL: int32(env.GetOrDefaultInt(EnvTTL, 300)), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + } +} + +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAccessKey, EnvSecretKey, EnvRegionID) + if err != nil { + return nil, fmt.Errorf("alicloud-esa: %w", err) + } + + config := NewDefaultConfig() + config.SecretID = values[EnvAccessKey] + config.SecretKey = values[EnvSecretKey] + config.RegionID = values[EnvRegionID] + + return NewDNSProviderConfig(config) +} + +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("alicloud-esa: the configuration of the DNS provider is nil") + } + + client, err := aliesa.NewClient(&aliopen.Config{ + AccessKeyId: tea.String(config.SecretID), + AccessKeySecret: tea.String(config.SecretKey), + Endpoint: tea.String(fmt.Sprintf("esa.%s.aliyuncs.com", config.RegionID)), + }) + if err != nil { + return nil, fmt.Errorf("alicloud-esa: %w", err) + } + + return &DNSProvider{ + client: client, + config: config, + }, nil +} + +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("alicloud-esa: could not find zone for domain %q: %w", domain, err) + } + + siteId, err := d.getSiteId(authZone) + if err != nil { + return fmt.Errorf("alicloud-esa: could not find site for zone %q: %w", authZone, err) + } + + if err := d.addOrUpdateDNSRecord(siteId, strings.TrimRight(info.EffectiveFQDN, "."), info.Value); err != nil { + return fmt.Errorf("alicloud-esa: %w", err) + } + + return nil +} + +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("alicloud-esa: could not find zone for domain %q: %w", domain, err) + } + + siteId, err := d.getSiteId(authZone) + if err != nil { + return fmt.Errorf("alicloud-esa: could not find site for zone %q: %w", authZone, err) + } + + if err := d.removeDNSRecord(siteId, strings.TrimRight(info.EffectiveFQDN, ".")); err != nil { + return fmt.Errorf("alicloud-esa: %w", err) + } + + return nil +} + +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) getSiteId(siteName string) (int64, error) { + d.siteIDsMtx.Lock() + siteID, ok := d.siteIDs[siteName] + d.siteIDsMtx.Unlock() + if ok { + return siteID, nil + } + + pageNumber := 1 + pageSize := 500 + for { + request := &aliesa.ListSitesRequest{ + SiteName: tea.String(siteName), + PageNumber: tea.Int32(int32(pageNumber)), + PageSize: tea.Int32(int32(pageNumber)), + AccessType: tea.String("NS"), + } + response, err := d.client.ListSites(request) + if err != nil { + return 0, err + } + + if response.Body == nil { + break + } else { + for _, record := range response.Body.Sites { + if tea.StringValue(record.SiteName) == siteName { + d.siteIDsMtx.Lock() + d.siteIDs[siteName] = *record.SiteId + d.siteIDsMtx.Unlock() + return *record.SiteId, nil + } + } + + if len(response.Body.Sites) < pageSize { + break + } + + pageNumber++ + } + } + + return 0, errors.New("failed to get site id") +} + +func (d *DNSProvider) findDNSRecord(siteId int64, effectiveFQDN string) (*aliesa.ListRecordsResponseBodyRecords, error) { + pageNumber := 1 + pageSize := 500 + for { + request := &aliesa.ListRecordsRequest{ + SiteId: tea.Int64(siteId), + Type: tea.String("TXT"), + RecordName: tea.String(effectiveFQDN), + PageNumber: tea.Int32(int32(pageNumber)), + PageSize: tea.Int32(int32(pageNumber)), + } + response, err := d.client.ListRecords(request) + if err != nil { + return nil, err + } + + if response.Body == nil { + break + } else { + for _, record := range response.Body.Records { + if tea.StringValue(record.RecordName) == effectiveFQDN { + return record, nil + } + } + + if len(response.Body.Records) < pageSize { + break + } + + pageNumber++ + } + } + + return nil, nil +} + +func (d *DNSProvider) addOrUpdateDNSRecord(siteId int64, effectiveFQDN, value string) error { + record, err := d.findDNSRecord(siteId, effectiveFQDN) + if err != nil { + return err + } + + if record == nil { + request := &aliesa.CreateRecordRequest{ + SiteId: tea.Int64(siteId), + Type: tea.String("TXT"), + RecordName: tea.String(effectiveFQDN), + Data: &aliesa.CreateRecordRequestData{ + Value: tea.String(value), + }, + Ttl: tea.Int32(d.config.TTL), + } + _, err := d.client.CreateRecord(request) + return err + } else { + request := &aliesa.UpdateRecordRequest{ + RecordId: record.RecordId, + Ttl: tea.Int32(d.config.TTL), + Data: &aliesa.UpdateRecordRequestData{ + Value: tea.String(value), + }, + } + _, err := d.client.UpdateRecord(request) + return err + } +} + +func (d *DNSProvider) removeDNSRecord(siteId int64, effectiveFQDN string) error { + record, err := d.findDNSRecord(siteId, effectiveFQDN) + if err != nil { + return err + } + + if record == nil { + return nil + } else { + request := &aliesa.DeleteRecordRequest{ + RecordId: record.RecordId, + } + _, err = d.client.DeleteRecord(request) + return err + } +} diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/internal/lego.go index 0c45f544..f67662b5 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/internal/lego.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/baiducloud/internal/lego.go @@ -128,7 +128,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } -func (d *DNSProvider) getDNSRecord(zoneName, subDomain string) (*bcedns.Record, error) { +func (d *DNSProvider) findDNSRecord(zoneName, subDomain string) (*bcedns.Record, error) { pageMarker := "" pageSize := 1000 for { @@ -159,7 +159,7 @@ func (d *DNSProvider) getDNSRecord(zoneName, subDomain string) (*bcedns.Record, } func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) error { - record, err := d.getDNSRecord(zoneName, subDomain) + record, err := d.findDNSRecord(zoneName, subDomain) if err != nil { return err } @@ -186,7 +186,7 @@ func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) er } func (d *DNSProvider) removeDNSRecord(zoneName, subDomain string) error { - record, err := d.getDNSRecord(zoneName, subDomain) + record, err := d.findDNSRecord(zoneName, subDomain) if err != nil { return err } diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6/internal/lego.go index 55404b65..8b33cf9e 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6/internal/lego.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6/internal/lego.go @@ -115,7 +115,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } -func (d *DNSProvider) getDNSRecord(zoneName, subDomain string) (*libdns.Record, error) { +func (d *DNSProvider) findDNSRecord(zoneName, subDomain string) (*libdns.Record, error) { records, err := d.client.GetRecords(context.Background(), zoneName) if err != nil { return nil, err @@ -131,7 +131,7 @@ func (d *DNSProvider) getDNSRecord(zoneName, subDomain string) (*libdns.Record, } func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) error { - record, err := d.getDNSRecord(zoneName, subDomain) + record, err := d.findDNSRecord(zoneName, subDomain) if err != nil { return err } @@ -153,7 +153,7 @@ func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) er } func (d *DNSProvider) removeDNSRecord(zoneName, subDomain string) error { - record, err := d.getDNSRecord(zoneName, subDomain) + record, err := d.findDNSRecord(zoneName, subDomain) if err != nil { return err } diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go index e4bf221e..7f1f5670 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go @@ -121,7 +121,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } -func (d *DNSProvider) getDNSRecord(zoneName, subDomain string) (*gnamesdk.ResolutionRecord, error) { +func (d *DNSProvider) findDNSRecord(zoneName, subDomain string) (*gnamesdk.ResolutionRecord, error) { page := int32(1) pageSize := int32(20) for { @@ -155,7 +155,7 @@ func (d *DNSProvider) getDNSRecord(zoneName, subDomain string) (*gnamesdk.Resolu } func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) error { - record, err := d.getDNSRecord(zoneName, subDomain) + record, err := d.findDNSRecord(zoneName, subDomain) if err != nil { return err } @@ -186,7 +186,7 @@ func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) er } func (d *DNSProvider) removeDNSRecord(zoneName, subDomain string) error { - record, err := d.getDNSRecord(zoneName, subDomain) + record, err := d.findDNSRecord(zoneName, subDomain) if err != nil { return err } diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal/lego.go index 14d86a4d..692c42d3 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal/lego.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal/lego.go @@ -20,7 +20,7 @@ const ( EnvSecretID = envNamespace + "SECRET_ID" EnvSecretKey = envNamespace + "SECRET_KEY" - EnvZoneId = envNamespace + "ZONE_ID" + EnvZoneID = envNamespace + "ZONE_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -33,7 +33,7 @@ var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { SecretID string SecretKey string - ZoneId string + ZoneID string PropagationTimeout time.Duration PollingInterval time.Duration @@ -56,7 +56,7 @@ func NewDefaultConfig() *Config { } func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvSecretID, EnvSecretKey, EnvZoneId) + values, err := env.Get(EnvSecretID, EnvSecretKey, EnvZoneID) if err != nil { return nil, fmt.Errorf("tencentcloud-eo: %w", err) } @@ -64,7 +64,7 @@ func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.SecretID = values[EnvSecretID] config.SecretKey = values[EnvSecretKey] - config.ZoneId = values[EnvSecretKey] + config.ZoneID = values[EnvSecretKey] return NewDNSProviderConfig(config) } @@ -112,12 +112,12 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } -func (d *DNSProvider) getDNSRecord(effectiveFQDN string) (*teo.DnsRecord, error) { +func (d *DNSProvider) findDNSRecord(effectiveFQDN string) (*teo.DnsRecord, error) { pageOffset := 0 pageLimit := 1000 for { request := teo.NewDescribeDnsRecordsRequest() - request.ZoneId = common.StringPtr(d.config.ZoneId) + request.ZoneId = common.StringPtr(d.config.ZoneID) request.Offset = common.Int64Ptr(int64(pageOffset)) request.Limit = common.Int64Ptr(int64(pageLimit)) request.Filters = []*teo.AdvancedFilter{ @@ -141,7 +141,7 @@ func (d *DNSProvider) getDNSRecord(effectiveFQDN string) (*teo.DnsRecord, error) } } - if len(response.Response.DnsRecords) < int(pageLimit) { + if len(response.Response.DnsRecords) < pageLimit { break } @@ -153,14 +153,14 @@ func (d *DNSProvider) getDNSRecord(effectiveFQDN string) (*teo.DnsRecord, error) } func (d *DNSProvider) addOrUpdateDNSRecord(effectiveFQDN, value string) error { - record, err := d.getDNSRecord(effectiveFQDN) + record, err := d.findDNSRecord(effectiveFQDN) if err != nil { return err } if record == nil { request := teo.NewCreateDnsRecordRequest() - request.ZoneId = common.StringPtr(d.config.ZoneId) + request.ZoneId = common.StringPtr(d.config.ZoneID) request.Name = common.StringPtr(effectiveFQDN) request.Type = common.StringPtr("TXT") request.Content = common.StringPtr(value) @@ -169,8 +169,9 @@ func (d *DNSProvider) addOrUpdateDNSRecord(effectiveFQDN, value string) error { return err } else { record.Content = common.StringPtr(value) + record.TTL = common.Int64Ptr(int64(d.config.TTL)) request := teo.NewModifyDnsRecordsRequest() - request.ZoneId = common.StringPtr(d.config.ZoneId) + request.ZoneId = common.StringPtr(d.config.ZoneID) request.DnsRecords = []*teo.DnsRecord{record} if _, err := d.client.ModifyDnsRecords(request); err != nil { return err @@ -178,7 +179,7 @@ func (d *DNSProvider) addOrUpdateDNSRecord(effectiveFQDN, value string) error { if *record.Status == "disable" { request := teo.NewModifyDnsRecordsStatusRequest() - request.ZoneId = common.StringPtr(d.config.ZoneId) + request.ZoneId = common.StringPtr(d.config.ZoneID) request.RecordsToEnable = []*string{record.RecordId} if _, err = d.client.ModifyDnsRecordsStatus(request); err != nil { return err @@ -190,7 +191,7 @@ func (d *DNSProvider) addOrUpdateDNSRecord(effectiveFQDN, value string) error { } func (d *DNSProvider) removeDNSRecord(effectiveFQDN string) error { - record, err := d.getDNSRecord(effectiveFQDN) + record, err := d.findDNSRecord(effectiveFQDN) if err != nil { return err } @@ -199,7 +200,7 @@ func (d *DNSProvider) removeDNSRecord(effectiveFQDN string) error { return nil } else { request := teo.NewDeleteDnsRecordsRequest() - request.ZoneId = common.StringPtr(d.config.ZoneId) + request.ZoneId = common.StringPtr(d.config.ZoneID) request.RecordIds = []*string{record.RecordId} _, err = d.client.DeleteDnsRecords(request) return err diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/tencentcloud_eo.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/tencentcloud_eo.go index 33552ecf..427c79ea 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/tencentcloud_eo.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/tencentcloud_eo.go @@ -24,7 +24,7 @@ func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, providerConfig := internal.NewDefaultConfig() providerConfig.SecretID = config.SecretId providerConfig.SecretKey = config.SecretKey - providerConfig.ZoneId = config.ZoneId + providerConfig.ZoneID = config.ZoneId if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } diff --git a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx index 37d7c6a1..57c448e2 100644 --- a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx @@ -41,6 +41,7 @@ import { useAccessesStore } from "@/stores/access"; import { useContactEmailsStore } from "@/stores/contact"; import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/validators"; +import ApplyNodeConfigFormAliyunESAConfig from "./ApplyNodeConfigFormAliyunESAConfig"; import ApplyNodeConfigFormAWSRoute53Config from "./ApplyNodeConfigFormAWSRoute53Config"; import ApplyNodeConfigFormHuaweiCloudDNSConfig from "./ApplyNodeConfigFormHuaweiCloudDNSConfig"; import ApplyNodeConfigFormJDCloudDNSConfig from "./ApplyNodeConfigFormJDCloudDNSConfig"; @@ -152,7 +153,7 @@ const ApplyNodeConfigForm = forwardRef { // 通常情况下每个授权信息只对应一个 DNS 提供商,此时无需显示 DNS 提供商字段; - // 如果对应多个(如 AWS 的 Route53、Lightsail,腾讯云的 DNS、EdgeOne 等),则显示。 + // 如果对应多个(如 AWS 的 Route53、Lightsail,阿里云的 DNS、ESA,腾讯云的 DNS、EdgeOne 等),则显示。 if (fieldProviderAccessId) { const access = accesses.find((e) => e.id === fieldProviderAccessId); const providers = Array.from(acmeDns01ProvidersMap.values()).filter((e) => e.provider === access?.provider); @@ -188,6 +189,8 @@ const ApplyNodeConfigForm = forwardRef; case ACME_DNS01_PROVIDERS.AWS: case ACME_DNS01_PROVIDERS.AWS_ROUTE53: return ; diff --git a/ui/src/components/workflow/node/ApplyNodeConfigFormAliyunESAConfig.tsx b/ui/src/components/workflow/node/ApplyNodeConfigFormAliyunESAConfig.tsx new file mode 100644 index 00000000..d429b53d --- /dev/null +++ b/ui/src/components/workflow/node/ApplyNodeConfigFormAliyunESAConfig.tsx @@ -0,0 +1,58 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +type ApplyNodeConfigFormAliyunESAConfigFieldValues = Nullish<{ + region: string; +}>; + +export type ApplyNodeConfigFormAliyunESAConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: ApplyNodeConfigFormAliyunESAConfigFieldValues; + onValuesChange?: (values: ApplyNodeConfigFormAliyunESAConfigFieldValues) => void; +}; + +const initFormModel = (): ApplyNodeConfigFormAliyunESAConfigFieldValues => { + return {}; +}; + +const ApplyNodeConfigFormAliyunESAConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: ApplyNodeConfigFormAliyunESAConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + region: z + .string({ message: t("workflow_node.apply.form.aliyun_esa_region.placeholder") }) + .nonempty(t("workflow_node.apply.form.aliyun_esa_region.placeholder")) + .trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default ApplyNodeConfigFormAliyunESAConfig; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 642a42da..219a2713 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -221,6 +221,7 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({ ACMEHTTPREQ: `${ACCESS_PROVIDERS.ACMEHTTPREQ}`, ALIYUN: `${ACCESS_PROVIDERS.ALIYUN}`, // 兼容旧值,等同于 `ALIYUN_DNS` ALIYUN_DNS: `${ACCESS_PROVIDERS.ALIYUN}-dns`, + ALIYUN_ESA: `${ACCESS_PROVIDERS.ALIYUN}-esa`, AWS: `${ACCESS_PROVIDERS.AWS}`, // 兼容旧值,等同于 `AWS_ROUTE53` AWS_ROUTE53: `${ACCESS_PROVIDERS.AWS}-route53`, AZURE: `${ACCESS_PROVIDERS.AZURE}`, // 兼容旧值,等同于 `AZURE_DNS` @@ -273,6 +274,7 @@ export const acmeDns01ProvidersMap: Maphttps://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint", "workflow_node.apply.form.aws_route53_region.label": "AWS Route53 Region", "workflow_node.apply.form.aws_route53_region.placeholder": "Please enter AWS Route53 region (e.g. us-east-1)", "workflow_node.apply.form.aws_route53_region.tooltip": "For more information, see https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 44cf3024..2233e93e 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -39,6 +39,9 @@ "workflow_node.apply.form.provider_access.placeholder": "请选择 DNS 提供商授权", "workflow_node.apply.form.provider_access.tooltip": "用于 ACME DNS-01 质询时操作域名解析记录,注意与部署阶段所需的主机提供商相区分。", "workflow_node.apply.form.provider_access.button": "新建", + "workflow_node.apply.form.aliyun_esa_region.label": "阿里云 ESA 服务地域", + "workflow_node.apply.form.aliyun_esa_region.placeholder": "请输入阿里云 ESA 服务地域(例如:cn-hangzhou)", + "workflow_node.apply.form.aliyun_esa_region.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-endpoint", "workflow_node.apply.form.aws_route53_region.label": "AWS Route53 服务区域", "workflow_node.apply.form.aws_route53_region.placeholder": "请输入 AWS Route53 服务区域(例如:us-east-1)", "workflow_node.apply.form.aws_route53_region.tooltip": "这是什么?请参阅 https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints", From 1499c637ee4c528081c1010b8b385b506024557f Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 7 May 2025 22:06:01 +0800 Subject: [PATCH 4/7] feat: new deployment provider: goedge --- internal/deployer/providers.go | 18 +++ internal/domain/access.go | 6 + internal/domain/provider.go | 3 +- .../providers/aliyun-ddos/aliyun_ddos_test.go | 2 +- .../core/deployer/providers/goedge/consts.go | 8 ++ .../core/deployer/providers/goedge/goedge.go | 131 ++++++++++++++++++ .../deployer/providers/goedge/goedge_test.go | 81 +++++++++++ .../providers/safeline/safeline_test.go | 2 +- internal/pkg/sdk3rd/goedge/api.go | 46 ++++++ internal/pkg/sdk3rd/goedge/client.go | 97 +++++++++++++ internal/pkg/sdk3rd/goedge/models.go | 52 +++++++ internal/pkg/sdk3rd/upyun/console/client.go | 5 +- ui/public/imgs/providers/goedge.png | Bin 0 -> 2902 bytes ui/src/components/access/AccessForm.tsx | 3 + .../access/AccessFormGoEdgeConfig.tsx | 87 ++++++++++++ .../workflow/node/DeployNodeConfigForm.tsx | 3 + .../node/DeployNodeConfigFormGoEdgeConfig.tsx | 79 +++++++++++ ui/src/domain/access.ts | 7 + ui/src/domain/provider.ts | 4 + ui/src/i18n/locales/en/nls.access.json | 9 ++ ui/src/i18n/locales/en/nls.provider.json | 1 - .../i18n/locales/en/nls.workflow.nodes.json | 5 + ui/src/i18n/locales/zh/nls.access.json | 9 ++ ui/src/i18n/locales/zh/nls.provider.json | 1 - .../i18n/locales/zh/nls.workflow.nodes.json | 5 + 25 files changed, 657 insertions(+), 7 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/goedge/consts.go create mode 100644 internal/pkg/core/deployer/providers/goedge/goedge.go create mode 100644 internal/pkg/core/deployer/providers/goedge/goedge_test.go create mode 100644 internal/pkg/sdk3rd/goedge/api.go create mode 100644 internal/pkg/sdk3rd/goedge/client.go create mode 100644 internal/pkg/sdk3rd/goedge/models.go create mode 100644 ui/public/imgs/providers/goedge.png create mode 100644 ui/src/components/access/AccessFormGoEdgeConfig.tsx create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormGoEdgeConfig.tsx diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 9dd36e32..ab92fa6f 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -41,6 +41,7 @@ import ( pDogeCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn" pEdgioApplications "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/edgio-applications" pGcoreCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/gcore-cdn" + pGoEdge "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/goedge" pHuaweiCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn" pHuaweiCloudELB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb" pHuaweiCloudSCM "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-scm" @@ -568,6 +569,23 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer } } + case domain.DeploymentProviderTypeGoEdge: + { + access := domain.AccessConfigForGoEdge{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + deployer, err := pGoEdge.NewDeployer(&pGoEdge.DeployerConfig{ + ApiUrl: access.ApiUrl, + AccessKeyId: access.AccessKeyId, + AccessKey: access.AccessKey, + ResourceType: pGoEdge.ResourceType(maputil.GetString(options.ProviderExtendedConfig, "resourceType")), + CertificateId: maputil.GetInt64(options.ProviderExtendedConfig, "certificateId"), + }) + return deployer, err + } + case domain.DeploymentProviderTypeHuaweiCloudCDN, domain.DeploymentProviderTypeHuaweiCloudELB, domain.DeploymentProviderTypeHuaweiCloudSCM, domain.DeploymentProviderTypeHuaweiCloudWAF: { access := domain.AccessConfigForHuaweiCloud{} diff --git a/internal/domain/access.go b/internal/domain/access.go index fd36417c..39d945a1 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -146,6 +146,12 @@ type AccessConfigForGoDaddy struct { ApiSecret string `json:"apiSecret"` } +type AccessConfigForGoEdge struct { + ApiUrl string `json:"apiUrl"` + AccessKeyId string `json:"accessKeyId"` + AccessKey string `json:"accessKey"` +} + type AccessConfigForGoogleTrustServices struct { EabKid string `json:"eabKid"` EabHmacKey string `json:"eabHmacKey"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 61af784f..2767d1ec 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -39,7 +39,7 @@ const ( AccessProviderTypeGname = AccessProviderType("gname") AccessProviderTypeGcore = AccessProviderType("gcore") AccessProviderTypeGoDaddy = AccessProviderType("godaddy") - AccessProviderTypeGoEdge = AccessProviderType("goedge") // GoEdge(预留) + AccessProviderTypeGoEdge = AccessProviderType("goedge") AccessProviderTypeGoogleTrustServices = AccessProviderType("googletrustservices") AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud") AccessProviderTypeJDCloud = AccessProviderType("jdcloud") @@ -186,6 +186,7 @@ const ( DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn") DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications") DeploymentProviderTypeGcoreCDN = DeploymentProviderType(AccessProviderTypeGcore + "-cdn") + DeploymentProviderTypeGoEdge = DeploymentProviderType(AccessProviderTypeGoEdge) DeploymentProviderTypeHuaweiCloudCDN = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-cdn") DeploymentProviderTypeHuaweiCloudELB = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-elb") DeploymentProviderTypeHuaweiCloudSCM = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-scm") diff --git a/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos_test.go b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos_test.go index b66f924f..b7f5ad34 100644 --- a/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos_test.go +++ b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos_test.go @@ -21,7 +21,7 @@ var ( ) func init() { - argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNDCDN_" + argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNDDOS_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") diff --git a/internal/pkg/core/deployer/providers/goedge/consts.go b/internal/pkg/core/deployer/providers/goedge/consts.go new file mode 100644 index 00000000..91eaa9a3 --- /dev/null +++ b/internal/pkg/core/deployer/providers/goedge/consts.go @@ -0,0 +1,8 @@ +package goedge + +type ResourceType string + +const ( + // 资源类型:替换指定证书。 + RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate") +) diff --git a/internal/pkg/core/deployer/providers/goedge/goedge.go b/internal/pkg/core/deployer/providers/goedge/goedge.go new file mode 100644 index 00000000..6aed4d56 --- /dev/null +++ b/internal/pkg/core/deployer/providers/goedge/goedge.go @@ -0,0 +1,131 @@ +package goedge + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "log/slog" + "net/url" + "time" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + goedgesdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/goedge" + certutil "github.com/usual2970/certimate/internal/pkg/utils/cert" +) + +type DeployerConfig struct { + // GoEdge URL。 + ApiUrl string `json:"apiUrl"` + // GoEdge 用户 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // GoEdge 用户 AccessKey。 + AccessKey string `json:"accessKey"` + // 部署资源类型。 + ResourceType ResourceType `json:"resourceType"` + // 证书 ID。 + // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 + CertificateId int64 `json:"certificateId,omitempty"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *goedgesdk.Client +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.ApiUrl, config.AccessKeyId, config.AccessKey) + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { + // 根据部署资源类型决定部署方式 + switch d.config.ResourceType { + case RESOURCE_TYPE_CERTIFICATE: + if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) + } + + return &deployer.DeployResult{}, nil +} + +func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error { + if d.config.CertificateId == 0 { + return errors.New("config `certificateId` is required") + } + + // 解析证书内容 + certX509, err := certutil.ParseCertificateFromPEM(certPEM) + if err != nil { + return err + } + + // 修改证书 + // REF: https://goedge.cloud/dev/api/service/SSLCertService?role=user#updateSSLCert + updateSSLCertReq := &goedgesdk.UpdateSSLCertRequest{ + SSLCertId: d.config.CertificateId, + IsOn: true, + Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), + Description: "upload from certimate", + ServerName: certX509.Subject.CommonName, + IsCA: false, + CertData: base64.StdEncoding.EncodeToString([]byte(certPEM)), + KeyData: base64.StdEncoding.EncodeToString([]byte(privkeyPEM)), + TimeBeginAt: certX509.NotBefore.Unix(), + TimeEndAt: certX509.NotAfter.Unix(), + DNSNames: certX509.DNSNames, + CommonNames: []string{certX509.Subject.CommonName}, + } + updateSSLCertResp, err := d.sdkClient.UpdateSSLCert(updateSSLCertReq) + d.logger.Debug("sdk request 'goedge.UpdateSSLCert'", slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'goedge.UpdateSSLCert': %w", err) + } + + return nil +} + +func createSdkClient(apiUrl, accessKeyId, accessKey string) (*goedgesdk.Client, error) { + if _, err := url.Parse(apiUrl); err != nil { + return nil, errors.New("invalid goedge api url") + } + + if accessKeyId == "" { + return nil, errors.New("invalid goedge access key id") + } + + if accessKey == "" { + return nil, errors.New("invalid goedge access key") + } + + client := goedgesdk.NewClient(apiUrl, "user", accessKeyId, accessKey) + return client, nil +} diff --git a/internal/pkg/core/deployer/providers/goedge/goedge_test.go b/internal/pkg/core/deployer/providers/goedge/goedge_test.go new file mode 100644 index 00000000..1f326b9e --- /dev/null +++ b/internal/pkg/core/deployer/providers/goedge/goedge_test.go @@ -0,0 +1,81 @@ +package goedge_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/goedge" +) + +var ( + fInputCertPath string + fInputKeyPath string + fApiUrl string + fAccessKeyId string + fAccessKey string + fCertificateId int +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_GOEDGE_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") + flag.IntVar(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") +} + +/* +Shell command to run this test: + + go test -v ./goedge_test.go -args \ + --CERTIMATE_DEPLOYER_GOEDGE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_GOEDGE_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_GOEDGE_APIURL="http://127.0.0.1:7788" \ + --CERTIMATE_DEPLOYER_GOEDGE_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_GOEDGE_ACCESSKEY="your-access-key" \ + --CERTIMATE_DEPLOYER_GOEDGE_CERTIFICATEID="your-cerficiate-id" +*/ +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("APIURL: %v", fApiUrl), + fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("ACCESSKEY: %v", fAccessKey), + fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + ApiUrl: fApiUrl, + AccessKeyId: fAccessKeyId, + AccessKey: fAccessKey, + ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, + CertificateId: int64(fCertificateId), + }) + 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/internal/pkg/core/deployer/providers/safeline/safeline_test.go b/internal/pkg/core/deployer/providers/safeline/safeline_test.go index b3bac993..294086c8 100644 --- a/internal/pkg/core/deployer/providers/safeline/safeline_test.go +++ b/internal/pkg/core/deployer/providers/safeline/safeline_test.go @@ -56,7 +56,7 @@ func TestDeploy(t *testing.T) { ApiUrl: fApiUrl, ApiToken: fApiToken, AllowInsecureConnections: true, - ResourceType: provider.ResourceType("certificate"), + ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, CertificateId: int32(fCertificateId), }) if err != nil { diff --git a/internal/pkg/sdk3rd/goedge/api.go b/internal/pkg/sdk3rd/goedge/api.go new file mode 100644 index 00000000..67ee9194 --- /dev/null +++ b/internal/pkg/sdk3rd/goedge/api.go @@ -0,0 +1,46 @@ +package goedge + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +func (c *Client) getAccessToken() error { + req := &getAPIAccessTokenRequest{ + Type: c.apiUserType, + AccessKeyId: c.accessKeyId, + AccessKey: c.accessKey, + } + res, err := c.sendRequest(http.MethodPost, "/APIAccessTokenService/getAPIAccessToken", req) + if err != nil { + return err + } + + resp := &getAPIAccessTokenResponse{} + if err := json.Unmarshal(res.Body(), &resp); err != nil { + return fmt.Errorf("goedge api error: failed to parse response: %w", err) + } else if resp.GetCode() != 200 { + return fmt.Errorf("goedge get access token failed: code: %d, message: %s", resp.GetCode(), resp.GetMessage()) + } + + c.accessTokenMtx.Lock() + c.accessToken = resp.Data.Token + c.accessTokenExp = time.Unix(resp.Data.ExpiresAt, 0) + c.accessTokenMtx.Unlock() + + return nil +} + +func (c *Client) UpdateSSLCert(req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) { + if c.accessToken == "" || c.accessTokenExp.Before(time.Now()) { + if err := c.getAccessToken(); err != nil { + return nil, err + } + } + + resp := &UpdateSSLCertResponse{} + err := c.sendRequestWithResult(http.MethodPost, "/SSLCertService/updateSSLCert", req, resp) + return resp, err +} diff --git a/internal/pkg/sdk3rd/goedge/client.go b/internal/pkg/sdk3rd/goedge/client.go new file mode 100644 index 00000000..c2e3b4f8 --- /dev/null +++ b/internal/pkg/sdk3rd/goedge/client.go @@ -0,0 +1,97 @@ +package goedge + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + apiHost string + apiUserType string + accessKeyId string + accessKey string + + accessToken string + accessTokenExp time.Time + accessTokenMtx sync.Mutex + + client *resty.Client +} + +func NewClient(apiHost, apiUserType, accessKeyId, accessKey string) *Client { + client := resty.New() + + return &Client{ + apiHost: strings.TrimRight(apiHost, "/"), + apiUserType: apiUserType, + accessKeyId: accessKeyId, + accessKey: accessKey, + client: client, + } +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) { + req := c.client.R().SetBasicAuth(c.accessKeyId, c.accessKey) + req.Method = method + req.URL = c.apiHost + path + if strings.EqualFold(method, http.MethodGet) { + qs := make(map[string]string) + if params != nil { + temp := make(map[string]any) + jsonb, _ := json.Marshal(params) + json.Unmarshal(jsonb, &temp) + for k, v := range temp { + if v != nil { + qs[k] = fmt.Sprintf("%v", v) + } + } + } + + req = req. + SetQueryParams(qs). + SetHeader("X-Edge-Access-Token", c.accessToken) + } else { + req = req. + SetHeader("Content-Type", "application/json"). + SetHeader("X-Edge-Access-Token", c.accessToken). + SetBody(params) + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("goedge api error: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("goedge api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) + } + + return resp, nil +} + +func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result BaseResponse) error { + resp, err := c.sendRequest(method, path, params) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &result) + } + return err + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return fmt.Errorf("goedge api error: failed to parse response: %w", err) + } else if errcode := result.GetCode(); errcode != 200 { + return fmt.Errorf("goedge api error: %d - %s", errcode, result.GetMessage()) + } + + return nil +} diff --git a/internal/pkg/sdk3rd/goedge/models.go b/internal/pkg/sdk3rd/goedge/models.go new file mode 100644 index 00000000..d19bb558 --- /dev/null +++ b/internal/pkg/sdk3rd/goedge/models.go @@ -0,0 +1,52 @@ +package goedge + +type BaseResponse interface { + GetCode() int32 + GetMessage() string +} + +type baseResponse struct { + Code int32 `json:"code"` + Message string `json:"message"` +} + +func (r *baseResponse) GetCode() int32 { + return r.Code +} + +func (r *baseResponse) GetMessage() string { + return r.Message +} + +type getAPIAccessTokenRequest struct { + Type string `json:"type"` + AccessKeyId string `json:"accessKeyId"` + AccessKey string `json:"accessKey"` +} + +type getAPIAccessTokenResponse struct { + baseResponse + Data *struct { + Token string `json:"token"` + ExpiresAt int64 `json:"expiresAt"` + } `json:"data,omitempty"` +} + +type UpdateSSLCertRequest struct { + SSLCertId int64 `json:"sslCertId"` + IsOn bool `json:"isOn"` + Name string `json:"name"` + Description string `json:"description"` + ServerName string `json:"serverName"` + IsCA bool `json:"isCA"` + CertData string `json:"certData"` + KeyData string `json:"keyData"` + TimeBeginAt int64 `json:"timeBeginAt"` + TimeEndAt int64 `json:"timeEndAt"` + DNSNames []string `json:"dnsNames"` + CommonNames []string `json:"commonNames"` +} + +type UpdateSSLCertResponse struct { + baseResponse +} diff --git a/internal/pkg/sdk3rd/upyun/console/client.go b/internal/pkg/sdk3rd/upyun/console/client.go index cf431c2c..56e7a86e 100644 --- a/internal/pkg/sdk3rd/upyun/console/client.go +++ b/internal/pkg/sdk3rd/upyun/console/client.go @@ -11,8 +11,9 @@ import ( ) type Client struct { - username string - password string + username string + password string + loginCookie string client *resty.Client diff --git a/ui/public/imgs/providers/goedge.png b/ui/public/imgs/providers/goedge.png new file mode 100644 index 0000000000000000000000000000000000000000..760ab333d3b797b20910e0841616f95757ab4082 GIT binary patch literal 2902 zcmaJ@c|4SB8=hgvlC_i?j%j?6!>kNuG8o1(efC|H88d^yEX@ovlCq>uC!us?$rh22 z>QLF2#5ofBDqGn(9E7sg3E$|PI^Q3s?|pvn`z*ify6)?_@B5GUf|G-doXmb1001CI zvL(8RX7lY=N>cQ_%{Vb6n)dOny!fu{0KS02r2_~wwm%(0Vp0O>E_4bl;>53X697P5 zi{a+Q_p-OcQQ1s5Wg7z*GC3kP0AOM!`$fp$jz)PzZyDhk9b{k@g%* zdJx0*B$w`b(!q^-GK7kyLCs7dCPJJ@fJx_5AVOv+i-!~9p`UeeqV@JM0t)#I;fLU% zf12{LcY;{5xpW8?j)GB2TyV^M}*xI{KJj7ewlzi?@Pb5Y;qZnp!IBZ^FZ7y5$xlFnkP_6oiJ|@2$bjl(mF4AR2JlNCAGzj2|C|d|>fEYN1>Q=wLzfhN6wZ@t6 z$TSlCRz~v-ANDx>qBbi+e{C?bYvEJpLN>3+mdL?rh&e3fgL40Ij8vFhPd6mCxU$}7 zp8x&S&dk?JLGdpijb;zN!kf5e*k4G}XP&wZM21I>?+9IwldsMZziA$1Z!xW+S@Urd z4F+7_!nCv03u6j{@p-m>Q?Z;%nWjh9?J`$n4Bt;C{dN$v=RG-TtWQ!^YX38^qxB`G z@}zQ;Lr(!=|12i@1_0Eo5GdQIqb>&SHL8!kTyG(E$08OS)u!=d^Pb!uGC=Vja4zad zXZ`Yl8zcX@)UYC-h2H7qQ)DW*T_vefdWE9{~)i>X(}i zxT>=1)Z*4Nhesz|s?%1L3ougsPG9O*{E`j0q=H_|`nmgo8h7oGX~>J^W{FqPm1ae} zM4M+QLZ^D-vU&V7wPQN=rrj;!YH|1Kx*vEJF;O+W;Rd_ol1u*}@zBJh)nq_v)x(uZ zbEE8QV%NY-XJ=h~kB9hFhYV#vhaB&cy1C0}T&IGq0L&i9?^Go)ih`O0fas>onb9NM zS|deHgh}S_w}MfsOrxW@1@m*ldO3NA-n{`s=N-17b67xclA&h_WS}Tt%P|`L8pXq^ zG+)!RtTtxly+ozX(eE`X9zi_Ui*JjRX&k6AQzKVzWwfp8BodwibZvQSO@++xo_jxN z=h(jUrjOh-CoB2f&mldIHj2Cr)P6HVO>|X1d-wSTcFf?i;VzZ582^N2AwS0~`=Zn6 z3iJ1UQh|DeTkgP*!BRG65aEWSj4w*gd0+tI@)p?-IrAL&cEKCUKNb~yQ+vZRtVBt- zPU&EOBK`8|oe*4AQU1io8%v7kQvYqt-q$=*C1x6_6+1=W(AI{ntYiwMnEc4NP|a48pma=0aDH|6e^!g(xEH=ea?I_`LS z`J*33jf`m6%05Tkg8b0jV!q@IjN$8{&X#UnWxPAm7-%C@L%SZHDpP}hS7IdLIpZXE z!7c{3?7#>%@upigRQNV#4GSo_evK2P*j{<-#dZyq+7GE?279ou1YTZW&3(B-yL4AO zVX1E{PW)ikvEC0+U4W7IORulMI8Q6(Ppv3RFWfz!(+_UU=^t-P zRCW_i{1`}&zB(z{Jfi@UYqBs*LpyLks@`hde1h5HI0C%fwa@D>Jq5;_=M!lcl>N@B z&3Ya?G&1jlzvZDAZVGPHE!P>TJ#>2BbLm3zJMxUT9-yBjS4xwq4hEK~whxz`+3mOr z0yzS;Z@{pF- z&JHE#ViQ!kkAD@xETfb|5Wt<9q$EcV>ne{c)Rm2nYKyOH$Bnc%FC1W3yp4!V7tcyb zoj)4gp**8MT?SiA7$+HqKgOn%3`>_r1{DlKPu4#9o;CI}zTeSV zHaeT-9q@dYid1YXFTW-@X_fEUSKcxnS-e(!thMKiZ_N0v{nl+hidG-o1b0G!DXZ4E z!zyO19wPc4Jjyzk_-3rhTtjwf>A>~08GD$yn9XU!`gLI!&|w z_LS39-QkNz8}~@ZeyV22P2%dSswV5(HU{yLh-4p$?>8v6m!y5N1P!Xd_cG)jL4o5P ziMz=<;ry0%;Exvl?^KTzo*o~)msJ@#b>qrLxvC<;xFuzO1%q*QD6I6DUS{jdx)kb* zY&}B39sh$hsak_p8-0E3V8OFjrmdyG^&#;JH`irlZ9)#}wf6%Q<9K%^OTe6(NKx05 z((r<~)Z}^33*X3P3fy|TCgDA$0I#~AB>CMYjk}Ke_f%eIrVZbOwF<9|sO)^EViLZQ zlPd&St*`hccxsF-R6qbPXaCili|y3kFBa0f_=R{xAyI+5>>5i85v&TJ5cC~qr2O`ZwOn>2j+5ggjM8nx#Bu){jJ$J9z$KE}5t zacyWvC3Odp2Mbz~Y22E1-qgrbj{eqovAFIP4FuFJ=CZ`><2SbdbV*hY#5)$`*#820 CL)@7F literal 0 HcmV?d00001 diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 85ad8a78..3727f09c 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -34,6 +34,7 @@ import AccessFormEmailConfig from "./AccessFormEmailConfig"; import AccessFormGcoreConfig from "./AccessFormGcoreConfig"; import AccessFormGnameConfig from "./AccessFormGnameConfig"; import AccessFormGoDaddyConfig from "./AccessFormGoDaddyConfig"; +import AccessFormGoEdgeConfig from "./AccessFormGoEdgeConfig"; import AccessFormGoogleTrustServicesConfig from "./AccessFormGoogleTrustServicesConfig"; import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig"; import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig"; @@ -200,6 +201,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.GODADDY: return ; + case ACCESS_PROVIDERS.GOEDGE: + return ; case ACCESS_PROVIDERS.GOOGLETRUSTSERVICES: return ; case ACCESS_PROVIDERS.EDGIO: diff --git a/ui/src/components/access/AccessFormGoEdgeConfig.tsx b/ui/src/components/access/AccessFormGoEdgeConfig.tsx new file mode 100644 index 00000000..a673aa56 --- /dev/null +++ b/ui/src/components/access/AccessFormGoEdgeConfig.tsx @@ -0,0 +1,87 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForGoEdge } from "@/domain/access"; + +type AccessFormGoEdgeConfigFieldValues = Nullish; + +export type AccessFormGoEdgeConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormGoEdgeConfigFieldValues; + onValuesChange?: (values: AccessFormGoEdgeConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormGoEdgeConfigFieldValues => { + return { + apiUrl: "http://:7788/", + accessKeyId: "", + accessKey: "", + }; +}; + +const AccessFormGoEdgeConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormGoEdgeConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiUrl: z.string().url(t("common.errmsg.url_invalid")), + accessKeyId: z + .string() + .min(1, t("access.form.goedge_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + accessKey: z + .string() + .min(1, t("access.form.goedge_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + + + } + > + + +
+ ); +}; + +export default AccessFormGoEdgeConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index a9c83a65..e2e4f811 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -47,6 +47,7 @@ import DeployNodeConfigFormCdnflyConfig from "./DeployNodeConfigFormCdnflyConfig import DeployNodeConfigFormDogeCloudCDNConfig from "./DeployNodeConfigFormDogeCloudCDNConfig"; import DeployNodeConfigFormEdgioApplicationsConfig from "./DeployNodeConfigFormEdgioApplicationsConfig"; import DeployNodeConfigFormGcoreCDNConfig from "./DeployNodeConfigFormGcoreCDNConfig"; +import DeployNodeConfigFormGoEdgeConfig from "./DeployNodeConfigFormGoEdgeConfig"; import DeployNodeConfigFormHuaweiCloudCDNConfig from "./DeployNodeConfigFormHuaweiCloudCDNConfig"; import DeployNodeConfigFormHuaweiCloudELBConfig from "./DeployNodeConfigFormHuaweiCloudELBConfig"; import DeployNodeConfigFormHuaweiCloudWAFConfig from "./DeployNodeConfigFormHuaweiCloudWAFConfig"; @@ -238,6 +239,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOYMENT_PROVIDERS.GCORE_CDN: return ; + case DEPLOYMENT_PROVIDERS.GOEDGE: + return ; case DEPLOYMENT_PROVIDERS.HUAWEICLOUD_CDN: return ; case DEPLOYMENT_PROVIDERS.HUAWEICLOUD_ELB: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormGoEdgeConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormGoEdgeConfig.tsx new file mode 100644 index 00000000..89dffb5f --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormGoEdgeConfig.tsx @@ -0,0 +1,79 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input, Select } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import Show from "@/components/Show"; + +type DeployNodeConfigFormGoEdgeConfigFieldValues = Nullish<{ + resourceType: string; + certificateId?: string | number; +}>; + +export type DeployNodeConfigFormGoEdgeConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormGoEdgeConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormGoEdgeConfigFieldValues) => void; +}; + +const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; + +const initFormModel = (): DeployNodeConfigFormGoEdgeConfigFieldValues => { + return { + resourceType: RESOURCE_TYPE_CERTIFICATE, + certificateId: "", + }; +}; + +const DeployNodeConfigFormGoEdgeConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormGoEdgeConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, { + message: t("workflow_node.deploy.form.goedge_resource_type.placeholder"), + }), + certificateId: z + .union([z.string(), z.number().int()]) + .nullish() + .refine((v) => { + if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; + return /^\d+$/.test(v + "") && +v! > 0; + }, t("workflow_node.deploy.form.goedge_certificate_id.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const fieldResourceType = Form.useWatch("resourceType", formInst); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + + + + + + + +
+ ); +}; + +export default DeployNodeConfigFormGoEdgeConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index b7c54306..f28de47b 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -31,6 +31,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForGcore | AccessConfigForGname | AccessConfigForGoDaddy + | AccessConfigForGoEdge | AccessConfigForGoogleTrustServices | AccessConfigForHuaweiCloud | AccessConfigForJDCloud @@ -194,6 +195,12 @@ export type AccessConfigForGoDaddy = { apiSecret: string; }; +export type AccessConfigForGoEdge = { + apiUrl: string; + accessKeyId: string; + accessKey: string; +}; + export type AccessConfigForGoogleTrustServices = { eabKid: string; eabHmacKey: string; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 219a2713..ea8744f4 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -30,6 +30,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ GCORE: "gcore", GNAME: "gname", GODADDY: "godaddy", + GOEDGE: "goedge", GOOGLETRUSTSERVICES: "googletrustservices", HUAWEICLOUD: "huaweicloud", JDCLOUD: "jdcloud", @@ -118,6 +119,7 @@ export const accessProvidersMap: Maphttps://developer.godaddy.com/", + "access.form.goedge_api_url.label": "GoEdge API URL", + "access.form.goedge_api_url.placeholder": "Please enter GoEdge API URL", + "access.form.goedge_api_url.tooltip": "For more information, see https://goedge.cloud/docs/API/Summary.md", + "access.form.goedge_access_key_id.label": "GoEdge user AccessKeyId", + "access.form.goedge_access_key_id.placeholder": "Please enter GoEdge user AccessKeyId", + "access.form.goedge_access_key_id.tooltip": "For more information, see https://goedge.cloud/docs/API/Auth.md", + "access.form.goedge_access_key.label": "GoEdge user AccessKey", + "access.form.goedge_access_key.placeholder": "Please enter GoEdge user AccessKey", + "access.form.goedge_access_key.tooltip": "For more information, see https://goedge.cloud/docs/API/Auth.md", "access.form.googletrustservices_eab_kid.label": "ACME EAB KID", "access.form.googletrustservices_eab_kid.placeholder": "Please enter ACME EAB KID", "access.form.googletrustservices_eab_kid.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index 6beb2a0a..8a191665 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -67,7 +67,6 @@ "provider.gname": "GNAME", "provider.godaddy": "GoDaddy", "provider.goedge": "GoEdge", - "provider.goedge.cdn": "GoEdge - CDN (Content Delivery Network)", "provider.googletrustservices": "Google Trust Services", "provider.huaweicloud": "Huawei Cloud", "provider.huaweicloud.cdn": "Huawei Cloud - CDN (Content Delivery Network)", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 08a59a43..6ebbba63 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -351,6 +351,11 @@ "workflow_node.deploy.form.gcore_cdn_resource_id.label": "Gcore CDN resource ID", "workflow_node.deploy.form.gcore_cdn_resource_id.placeholder": "Please enter Gcore CDN resource ID", "workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "For more information, see https://cdn.gcore.com/resources/list", + "workflow_node.deploy.form.goedge_resource_type.label": "Resource type", + "workflow_node.deploy.form.goedge_resource_type.placeholder": "Please select resource type", + "workflow_node.deploy.form.goedge_resource_type.option.certificate.label": "Certificate", + "workflow_node.deploy.form.goedge_certificate_id.label": "GoEdge certificate ID", + "workflow_node.deploy.form.goedge_certificate_id.placeholder": "Please enter GoEdge certificate ID", "workflow_node.deploy.form.huaweicloud_cdn_region.label": "Huawei Cloud CDN region", "workflow_node.deploy.form.huaweicloud_cdn_region.placeholder": "Please enter Huawei Cloud CDN region (e.g. cn-north-1)", "workflow_node.deploy.form.huaweicloud_cdn_region.tooltip": "For more information, see https://console-intl.huaweicloud.com/apiexplorer/#/endpoint", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 04f74b58..5cb1a038 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -194,6 +194,15 @@ "access.form.godaddy_api_secret.label": "GoDaddy API Secret", "access.form.godaddy_api_secret.placeholder": "请输入 GoDaddy API Secret", "access.form.godaddy_api_secret.tooltip": "这是什么?请参阅 https://developer.godaddy.com/", + "access.form.goedge_api_url.label": "GoEdge API URL", + "access.form.goedge_api_url.placeholder": "请输入 GoEdge API URL", + "access.form.goedge_api_url.tooltip": "这是什么?请参阅 https://goedge.cloud/docs/API/Summary.md", + "access.form.goedge_access_key_id.label": "GoEdge 用户 AccessKeyId", + "access.form.goedge_access_key_id.placeholder": "请输入 GoEdge 用户 AccessKeyId", + "access.form.goedge_access_key_id.tooltip": "这是什么?请参阅 https://goedge.cloud/docs/API/Auth.md", + "access.form.goedge_access_key.label": "GoEdge 用户 AccessKey", + "access.form.goedge_access_key.placeholder": "请输入 GoEdge 用户 AccessKey", + "access.form.goedge_access_key.tooltip": "这是什么?请参阅 https://goedge.cloud/docs/API/Auth.md", "access.form.googletrustservices_eab_kid.label": "ACME EAB KID", "access.form.googletrustservices_eab_kid.placeholder": "请输入 ACME EAB KID", "access.form.googletrustservices_eab_kid.tooltip": "这是什么?请参阅 https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 00e45abf..207dfc37 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -67,7 +67,6 @@ "provider.gname": "GNAME", "provider.godaddy": "GoDaddy", "provider.goedge": "GoEdge", - "provider.goedge.cdn": "GoEdge - 内容分发网络 CDN", "provider.googletrustservices": "Google Trust Services", "provider.huaweicloud": "华为云", "provider.huaweicloud.cdn": "华为云 - 内容分发网络 CDN", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 2233e93e..28e4b54d 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -350,6 +350,11 @@ "workflow_node.deploy.form.gcore_cdn_resource_id.label": "Gcore CDN 资源 ID", "workflow_node.deploy.form.gcore_cdn_resource_id.placeholder": "请输入 Gcore CDN 资源 ID", "workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "这是什么?请参阅 https://cdn.gcore.com/resources/list", + "workflow_node.deploy.form.goedge_resource_type.label": "证书替换方式", + "workflow_node.deploy.form.goedge_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.goedge_resource_type.option.certificate.label": "替换指定证书", + "workflow_node.deploy.form.goedge_certificate_id.label": "GoEdge 证书 ID", + "workflow_node.deploy.form.goedge_certificate_id.placeholder": "请输入 GoEdge 证书 ID", "workflow_node.deploy.form.huaweicloud_cdn_region.label": "华为云 CDN 服务区域", "workflow_node.deploy.form.huaweicloud_cdn_region.placeholder": "请输入华为云 CDN 服务区域(例如:cn-north-1)", "workflow_node.deploy.form.huaweicloud_cdn_region.tooltip": "这是什么?请参阅 https://console.huaweicloud.com/apiexplorer/#/endpoint", From 809f2319811f55bb801b781459c019e36cbb9dad Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 7 May 2025 22:15:11 +0800 Subject: [PATCH 5/7] feat: set the default max workers to the number of available CPU cores --- internal/workflow/dispatcher/dispatcher.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/workflow/dispatcher/dispatcher.go b/internal/workflow/dispatcher/dispatcher.go index f25fadcf..7874b945 100644 --- a/internal/workflow/dispatcher/dispatcher.go +++ b/internal/workflow/dispatcher/dispatcher.go @@ -15,12 +15,17 @@ import ( sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice" ) -var maxWorkers = runtime.NumCPU() +var maxWorkers = 1 func init() { envMaxWorkers := os.Getenv("CERTIMATE_WORKFLOW_MAX_WORKERS") if n, err := strconv.Atoi(envMaxWorkers); err != nil && n > 0 { maxWorkers = n + } else { + maxWorkers = runtime.GOMAXPROCS(0) + if maxWorkers == 0 { + maxWorkers = max(1, runtime.NumCPU()) + } } } From a4d397f24b362061871b6cfb613af5e69c07ed25 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 8 May 2025 15:36:51 +0800 Subject: [PATCH 6/7] fix: fix typo --- internal/domain/provider.go | 4 ++-- ui/src/pages/dashboard/Dashboard.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 2767d1ec..ffc33b5f 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -26,8 +26,8 @@ const ( AccessProviderTypeCloudflare = AccessProviderType("cloudflare") AccessProviderTypeClouDNS = AccessProviderType("cloudns") AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud") - AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 联通云(预留) - AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留) + AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 天翼云(预留) + AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 联通云(预留) AccessProviderTypeDeSEC = AccessProviderType("desec") AccessProviderTypeDingTalkBot = AccessProviderType("dingtalkbot") AccessProviderTypeDNSLA = AccessProviderType("dnsla") diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index 919b53d3..9915a8a7 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -310,7 +310,7 @@ const StatisticCard = ({ onClick?: () => void; }) => { return ( - + {icon} Date: Thu, 8 May 2025 20:43:09 +0800 Subject: [PATCH 7/7] feat: add preset scripts --- .../node/DeployNodeConfigFormLocalConfig.tsx | 250 +++++++++--------- .../node/DeployNodeConfigFormSSHConfig.tsx | 52 ++-- .../i18n/locales/en/nls.workflow.nodes.json | 19 +- .../i18n/locales/zh/nls.workflow.nodes.json | 19 +- 4 files changed, 181 insertions(+), 159 deletions(-) diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx index bf9386cc..2ebae25d 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx @@ -45,6 +45,108 @@ const initFormModel = (): DeployNodeConfigFormLocalConfigFieldValues => { }; }; +export const initPresetScript = ( + key: "sh_backup_files" | "ps_backup_files" | "sh_reload_nginx" | "ps_binding_iis" | "ps_binding_netsh" | "ps_binding_rdp", + params?: { + certPath?: string; + keyPath?: string; + pfxPassword?: string; + jksAlias?: string; + jksKeypass?: string; + jksStorepass?: string; + } +) => { + switch (key) { + case "sh_backup_files": + return `# 请将以下路径替换为实际值 +cp "${params?.certPath || ""}" "${params?.certPath || ""}.bak" 2>/dev/null || : +cp "${params?.keyPath || ""}" "${params?.keyPath || ""}.bak" 2>/dev/null || : + `.trim(); + + case "ps_backup_files": + return `# 请将以下路径替换为实际值 +if (Test-Path -Path "${params?.certPath || ""}" -PathType Leaf) { + Copy-Item -Path "${params?.certPath || ""}" -Destination "${params?.certPath || ""}.bak" -Force +} +if (Test-Path -Path "${params?.keyPath || ""}" -PathType Leaf) { + Copy-Item -Path "${params?.keyPath || ""}" -Destination "${params?.keyPath || ""}.bak" -Force +} + `.trim(); + + case "sh_reload_nginx": + return `sudo service nginx reload`; + + case "ps_binding_iis": + return `# 需要管理员权限 +# 请将以下变量替换为实际值 +$pfxPath = "${params?.certPath || ""}" # PFX 文件路径 +$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码 +$siteName = "" # IIS 网站名称 +$domain = "" # 域名 +$ipaddr = "" # 绑定 IP,“*”表示所有 IP 绑定 +$port = "" # 绑定端口 + + +# 导入证书到本地计算机的个人存储区 +$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable +# 获取 Thumbprint +$thumbprint = $cert.Thumbprint +# 导入 WebAdministration 模块 +Import-Module WebAdministration +# 检查是否已存在 HTTPS 绑定 +$existingBinding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -HostHeader "$domain" -ErrorAction SilentlyContinue +if (!$existingBinding) { + # 添加新的 HTTPS 绑定 + New-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" +} +# 获取绑定对象 +$binding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" +# 绑定 SSL 证书 +$binding.AddSslCertificate($thumbprint, "My") +# 删除目录下的证书文件 +Remove-Item -Path "$pfxPath" -Force + `.trim(); + + case "ps_binding_netsh": + return `# 需要管理员权限 +# 请将以下变量替换为实际值 +$pfxPath = "${params?.certPath || ""}" # PFX 文件路径 +$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码 +$ipaddr = "" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名。 +$port = "" # 绑定端口 + +$addr = $ipaddr + ":" + $port + +# 导入证书到本地计算机的个人存储区 +$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable +# 获取 Thumbprint +$thumbprint = $cert.Thumbprint +# 检测端口是否绑定证书,如绑定则删除绑定 +$isExist = netsh http show sslcert ipport=$addr +if ($isExist -like "*$addr*"){ netsh http delete sslcert ipport=$addr } +# 绑定到端口 +netsh http add sslcert ipport=$addr certhash=$thumbprint +# 删除目录下的证书文件 +Remove-Item -Path "$pfxPath" -Force + `.trim(); + + case "ps_binding_rdp": + return `# 需要管理员权限 +# 请将以下变量替换为实际值 +$pfxPath = "${params?.certPath || ""}" # PFX 文件路径 +$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码 + +# 导入证书到本地计算机的个人存储区 +$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable +# 获取 Thumbprint +$thumbprint = $cert.Thumbprint +# 绑定到 RDP +$rdpCertPath = "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp" +Set-ItemProperty -Path $rdpCertPath -Name "SSLCertificateSHA1Hash" -Value "$thumbprint" + `.trim(); + } +}; + const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormLocalConfigProps) => { const { t } = useTranslation(); @@ -136,16 +238,15 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i const handlePresetPreScriptClick = (key: string) => { switch (key) { - case "backup_files": + case "sh_backup_files": + case "ps_backup_files": { + const presetScriptParams = { + certPath: formInst.getFieldValue("certPath"), + keyPath: formInst.getFieldValue("keyPath"), + }; formInst.setFieldValue("shellEnv", SHELLENV_SH); - formInst.setFieldValue( - "preCommand", - `# 请将以下路径替换为实际值 -cp "${formInst.getFieldValue("certPath") || ""}" "${formInst.getFieldValue("certPath") || ""}.bak" 2>/dev/null || : -cp "${formInst.getFieldValue("keyPath") || ""}" "${formInst.getFieldValue("keyPath") || ""}.bak" 2>/dev/null || : - `.trim() - ); + formInst.setFieldValue("preCommand", initPresetScript(key, presetScriptParams)); } break; } @@ -153,97 +254,23 @@ cp "${formInst.getFieldValue("keyPath") || ""}" "${formInst.getFi const handlePresetPostScriptClick = (key: string) => { switch (key) { - case "reload_nginx": + case "sh_reload_nginx": { formInst.setFieldValue("shellEnv", SHELLENV_SH); - formInst.setFieldValue("postCommand", "sudo service nginx reload"); + formInst.setFieldValue("postCommand", initPresetScript(key)); } break; - case "binding_iis": + case "ps_binding_iis": + case "ps_binding_netsh": + case "ps_binding_rdp": { + const presetScriptParams = { + certPath: formInst.getFieldValue("certPath"), + pfxPassword: formInst.getFieldValue("pfxPassword"), + }; formInst.setFieldValue("shellEnv", SHELLENV_POWERSHELL); - formInst.setFieldValue( - "postCommand", - `# 请将以下变量替换为实际值 -$pfxPath = "${formInst.getFieldValue("certPath") || ""}" # PFX 文件路径 -$pfxPassword = "${formInst.getFieldValue("pfxPassword") || ""}" # PFX 密码 -$siteName = "" # IIS 网站名称 -$domain = "" # 域名 -$ipaddr = "" # 绑定 IP,“*”表示所有 IP 绑定 -$port = "" # 绑定端口 - - -# 导入证书到本地计算机的个人存储区 -$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable -# 获取 Thumbprint -$thumbprint = $cert.Thumbprint -# 导入 WebAdministration 模块 -Import-Module WebAdministration -# 检查是否已存在 HTTPS 绑定 -$existingBinding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -HostHeader "$domain" -ErrorAction SilentlyContinue -if (!$existingBinding) { - # 添加新的 HTTPS 绑定 - New-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" -} -# 获取绑定对象 -$binding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" -# 绑定 SSL 证书 -$binding.AddSslCertificate($thumbprint, "My") -# 删除目录下的证书文件 -Remove-Item -Path "$pfxPath" -Force - `.trim() - ); - } - break; - - case "binding_netsh": - { - formInst.setFieldValue("shellEnv", SHELLENV_POWERSHELL); - formInst.setFieldValue( - "postCommand", - `# 请将以下变量替换为实际值 -$pfxPath = "${formInst.getFieldValue("certPath") || ""}" # PFX 文件路径 -$pfxPassword = "${formInst.getFieldValue("pfxPassword") || ""}" # PFX 密码 -$ipaddr = "" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名。 -$port = "" # 绑定端口 - -$addr = $ipaddr + ":" + $port - -# 导入证书到本地计算机的个人存储区 -$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable -# 获取 Thumbprint -$thumbprint = $cert.Thumbprint -# 检测端口是否绑定证书,如绑定则删除绑定 -$isExist = netsh http show sslcert ipport=$addr -if ($isExist -like "*$addr*"){ netsh http delete sslcert ipport=$addr } -# 绑定到端口 -netsh http add sslcert ipport=$addr certhash=$thumbprint -# 删除目录下的证书文件 -Remove-Item -Path "$pfxPath" -Force - `.trim() - ); - } - break; - - case "binding_rdp": - { - formInst.setFieldValue("shellEnv", SHELLENV_POWERSHELL); - formInst.setFieldValue( - "postCommand", - `# 请将以下变量替换为实际值 -$pfxPath = "${formInst.getFieldValue("certPath") || ""}" # PFX 文件路径 -$pfxPassword = "${formInst.getFieldValue("pfxPassword") || ""}" # PFX 密码 - -# 导入证书到本地计算机的个人存储区 -$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable -# 获取 Thumbprint -$thumbprint = $cert.Thumbprint -# 绑定到 RDP -$rdpCertPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" -Set-ItemProperty -Path $rdpCertPath -Name "SSLCertificateSHA1Hash" -Value "$thumbprint" - `.trim() - ); + formInst.setFieldValue("postCommand", initPresetScript(key, presetScriptParams)); } break; } @@ -359,13 +386,11 @@ Set-ItemProperty -Path $rdpCertPath -Name "SSLCertificateSHA1Hash" -Value "$thum
handlePresetPreScriptClick("backup_files"), - }, - ], + items: ["sh_backup_files", "ps_backup_files"].map((key) => ({ + key, + label: t(`workflow_node.deploy.form.local_preset_scripts.option.${key}.label`), + onClick: () => handlePresetPreScriptClick(key), + })), }} trigger={["click"]} > @@ -391,28 +416,11 @@ Set-ItemProperty -Path $rdpCertPath -Name "SSLCertificateSHA1Hash" -Value "$thum
handlePresetPostScriptClick("reload_nginx"), - }, - { - key: "binding_iis", - label: t("workflow_node.deploy.form.local_preset_scripts.option.binding_iis.label"), - onClick: () => handlePresetPostScriptClick("binding_iis"), - }, - { - key: "binding_netsh", - label: t("workflow_node.deploy.form.local_preset_scripts.option.binding_netsh.label"), - onClick: () => handlePresetPostScriptClick("binding_netsh"), - }, - { - key: "binding_rdp", - label: t("workflow_node.deploy.form.local_preset_scripts.option.binding_rdp.label"), - onClick: () => handlePresetPostScriptClick("binding_rdp"), - }, - ], + items: ["sh_reload_nginx", "ps_binding_iis", "ps_binding_netsh", "ps_binding_rdp"].map((key) => ({ + key, + label: t(`workflow_node.deploy.form.local_preset_scripts.option.${key}.label`), + onClick: () => handlePresetPostScriptClick(key), + })), }} trigger={["click"]} > diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx index 5f13e29d..21d76222 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx @@ -7,6 +7,8 @@ import { z } from "zod"; import Show from "@/components/Show"; import { CERTIFICATE_FORMATS } from "@/domain/certificate"; +import { initPresetScript } from "./DeployNodeConfigFormLocalConfig"; + type DeployNodeConfigFormSSHConfigFieldValues = Nullish<{ format: string; certPath: string; @@ -129,15 +131,14 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini const handlePresetPreScriptClick = (key: string) => { switch (key) { - case "backup_files": + case "sh_backup_files": + case "ps_backup_files": { - formInst.setFieldValue( - "preCommand", - `# 请将以下路径替换为实际值 -cp "${formInst.getFieldValue("certPath") || ""}" "${formInst.getFieldValue("certPath") || ""}.bak" 2>/dev/null || : -cp "${formInst.getFieldValue("keyPath") || ""}" "${formInst.getFieldValue("keyPath") || ""}.bak" 2>/dev/null || : - `.trim() - ); + const presetScriptParams = { + certPath: formInst.getFieldValue("certPath"), + keyPath: formInst.getFieldValue("keyPath"), + }; + formInst.setFieldValue("preCommand", initPresetScript(key, presetScriptParams)); } break; } @@ -145,9 +146,16 @@ cp "${formInst.getFieldValue("keyPath") || ""}" "${formInst.getFi const handlePresetPostScriptClick = (key: string) => { switch (key) { - case "reload_nginx": + case "sh_reload_nginx": + case "ps_binding_iis": + case "ps_binding_netsh": + case "ps_binding_rdp": { - formInst.setFieldValue("postCommand", "sudo service nginx reload"); + const presetScriptParams = { + certPath: formInst.getFieldValue("certPath"), + pfxPassword: formInst.getFieldValue("pfxPassword"), + }; + formInst.setFieldValue("postCommand", initPresetScript(key, presetScriptParams)); } break; } @@ -253,13 +261,11 @@ cp "${formInst.getFieldValue("keyPath") || ""}" "${formInst.getFi
handlePresetPreScriptClick("backup_files"), - }, - ], + items: ["sh_backup_files", "ps_backup_files"].map((key) => ({ + key, + label: t(`workflow_node.deploy.form.ssh_preset_scripts.option.${key}.label`), + onClick: () => handlePresetPreScriptClick(key), + })), }} trigger={["click"]} > @@ -285,13 +291,11 @@ cp "${formInst.getFieldValue("keyPath") || ""}" "${formInst.getFi
handlePresetPostScriptClick("reload_nginx"), - }, - ], + items: ["sh_reload_nginx", "ps_binding_iis", "ps_binding_netsh", "ps_binding_rdp"].map((key) => ({ + key, + label: t(`workflow_node.deploy.form.ssh_preset_scripts.option.${key}.label`), + onClick: () => handlePresetPostScriptClick(key), + })), }} trigger={["click"]} > diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 6ebbba63..60e6641f 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -466,11 +466,12 @@ "workflow_node.deploy.form.local_post_command.label": "Post-command (Optional)", "workflow_node.deploy.form.local_post_command.placeholder": "Please enter command to be executed after saving files", "workflow_node.deploy.form.local_preset_scripts.button": "Use preset scripts", - "workflow_node.deploy.form.local_preset_scripts.option.backup_files.label": "POSIX Bash - Backup certificate files", - "workflow_node.deploy.form.local_preset_scripts.option.reload_nginx.label": "POSIX Bash - Reload nginx", - "workflow_node.deploy.form.local_preset_scripts.option.binding_iis.label": "PowerShell - Binding IIS", - "workflow_node.deploy.form.local_preset_scripts.option.binding_netsh.label": "PowerShell - Binding netsh", - "workflow_node.deploy.form.local_preset_scripts.option.binding_rdp.label": "PowerShell - Binding RDP", + "workflow_node.deploy.form.local_preset_scripts.option.sh_backup_files.label": "POSIX Bash - Backup certificate files", + "workflow_node.deploy.form.local_preset_scripts.option.ps_backup_files.label": "PowerShell - Backup certificate files", + "workflow_node.deploy.form.local_preset_scripts.option.sh_reload_nginx.label": "POSIX Bash - Reload nginx", + "workflow_node.deploy.form.local_preset_scripts.option.ps_binding_iis.label": "PowerShell - Binding IIS", + "workflow_node.deploy.form.local_preset_scripts.option.ps_binding_netsh.label": "PowerShell - Binding netsh", + "workflow_node.deploy.form.local_preset_scripts.option.ps_.label": "PowerShell - Binding RDP", "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", @@ -524,8 +525,12 @@ "workflow_node.deploy.form.ssh_post_command.label": "Post-command (Optional)", "workflow_node.deploy.form.ssh_post_command.placeholder": "Please enter command to be executed after uploading files", "workflow_node.deploy.form.ssh_preset_scripts.button": "Use preset scripts", - "workflow_node.deploy.form.ssh_preset_scripts.option.backup_files.label": "POSIX Bash - Backup certificate files", - "workflow_node.deploy.form.ssh_preset_scripts.option.reload_nginx.label": "POSIX Bash - Reload nginx", + "workflow_node.deploy.form.ssh_preset_scripts.option.sh_backup_files.label": "POSIX Bash - Backup certificate files", + "workflow_node.deploy.form.ssh_preset_scripts.option.ps_backup_files.label": "PowerShell - Backup certificate files", + "workflow_node.deploy.form.ssh_preset_scripts.option.sh_reload_nginx.label": "POSIX Bash - Reload nginx", + "workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_iis.label": "PowerShell - Binding IIS", + "workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_netsh.label": "PowerShell - Binding netsh", + "workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_rdp.label": "PowerShell - Binding RDP", "workflow_node.deploy.form.ssh_use_scp.label": "Fallback to use SCP", "workflow_node.deploy.form.ssh_use_scp.tooltip": "If the remote server does not support SFTP, please enable this option to fallback to SCP.", "workflow_node.deploy.form.tencentcloud_cdn_domain.label": "Tencent Cloud CDN domain", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 28e4b54d..cdff1cf9 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -465,11 +465,12 @@ "workflow_node.deploy.form.local_post_command.label": "后置命令(可选)", "workflow_node.deploy.form.local_post_command.placeholder": "请输入保存文件后执行的命令", "workflow_node.deploy.form.local_preset_scripts.button": "使用预设脚本", - "workflow_node.deploy.form.local_preset_scripts.option.backup_files.label": "POSIX Bash - 备份原证书文件", - "workflow_node.deploy.form.local_preset_scripts.option.reload_nginx.label": "POSIX Bash - 重启 nginx 进程", - "workflow_node.deploy.form.local_preset_scripts.option.binding_iis.label": "PowerShell - 导入并绑定到 IIS(需管理员权限)", - "workflow_node.deploy.form.local_preset_scripts.option.binding_netsh.label": "PowerShell - 导入并绑定到 netsh(需管理员权限)", - "workflow_node.deploy.form.local_preset_scripts.option.binding_rdp.label": "PowerShell - 导入并绑定到 远程桌面连接(需管理员权限)", + "workflow_node.deploy.form.local_preset_scripts.option.sh_backup_files.label": "POSIX Bash - 备份原证书文件", + "workflow_node.deploy.form.local_preset_scripts.option.ps_backup_files.label": "PowerShell - 备份原证书文件", + "workflow_node.deploy.form.local_preset_scripts.option.sh_reload_nginx.label": "POSIX Bash - 重启 nginx 进程", + "workflow_node.deploy.form.local_preset_scripts.option.ps_binding_iis.label": "PowerShell - 导入并绑定到 IIS", + "workflow_node.deploy.form.local_preset_scripts.option.ps_binding_netsh.label": "PowerShell - 导入并绑定到 netsh", + "workflow_node.deploy.form.local_preset_scripts.option.ps_binding_rdp.label": "PowerShell - 导入并绑定到 RDP", "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", @@ -523,8 +524,12 @@ "workflow_node.deploy.form.ssh_post_command.label": "后置命令(可选)", "workflow_node.deploy.form.ssh_post_command.placeholder": "请输入保存文件后执行的命令", "workflow_node.deploy.form.ssh_preset_scripts.button": "使用预设脚本", - "workflow_node.deploy.form.ssh_preset_scripts.option.backup_files.label": "POSIX Bash - 备份原证书文件", - "workflow_node.deploy.form.ssh_preset_scripts.option.reload_nginx.label": "POSIX Bash - 重启 nginx 进程", + "workflow_node.deploy.form.ssh_preset_scripts.option.sh_backup_files.label": "POSIX Bash - 备份原证书文件", + "workflow_node.deploy.form.ssh_preset_scripts.option.ps_backup_files.label": "PowerShell - 备份原证书文件", + "workflow_node.deploy.form.ssh_preset_scripts.option.sh_reload_nginx.label": "POSIX Bash - 重启 nginx 进程", + "workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_iis.label": "PowerShell - 导入并绑定到 IIS", + "workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_netsh.label": "PowerShell - 导入并绑定到 netsh", + "workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_rdp.label": "PowerShell - 导入并绑定到 RDP", "workflow_node.deploy.form.ssh_use_scp.label": "回退使用 SCP", "workflow_node.deploy.form.ssh_use_scp.tooltip": "如果你的远程服务器不支持 SFTP,请开启此选项回退为 SCP。", "workflow_node.deploy.form.tencentcloud_cdn_domain.label": "腾讯云 CDN 加速域名",