From c72dc0d2c40530cffa906aa9c00c4ee2ff3f28fa Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 17 Feb 2025 16:48:33 +0800 Subject: [PATCH] feat: add safeline deployer --- README.md | 1 + README_EN.md | 3 +- internal/deployer/providers.go | 23 +++- internal/domain/access.go | 5 + internal/domain/provider.go | 3 +- .../huaweicloud-waf/huaweicloud_waf.go | 4 +- .../deployer/providers/safeline/consts.go | 8 ++ .../deployer/providers/safeline/safeline.go | 102 ++++++++++++++++++ .../providers/safeline/safeline_test.go | 76 +++++++++++++ internal/pkg/vendors/btpanel-sdk/client.go | 8 +- internal/pkg/vendors/gname-sdk/api.go | 59 ++++------ internal/pkg/vendors/gname-sdk/client.go | 12 +-- internal/pkg/vendors/safeline-sdk/api.go | 34 ++++++ internal/pkg/vendors/safeline-sdk/client.go | 87 +++++++++++++++ ui/public/imgs/providers/safeline.svg | 1 + ui/src/components/access/AccessForm.tsx | 3 + .../access/AccessFormBaotaPanelConfig.tsx | 2 +- .../access/AccessFormPowerDNSConfig.tsx | 2 +- .../access/AccessFormSafeLineConfig.tsx | 72 +++++++++++++ .../provider/AccessProviderSelect.tsx | 12 ++- .../provider/ApplyDNSProviderSelect.tsx | 12 ++- .../provider/DeployProviderSelect.tsx | 12 ++- .../workflow/node/DeployNodeConfigForm.tsx | 5 +- .../DeployNodeConfigFormSafeLineConfig.tsx | 85 +++++++++++++++ ui/src/domain/access.ts | 6 ++ ui/src/domain/provider.ts | 4 + ui/src/i18n/locales/en/nls.access.json | 6 ++ .../i18n/locales/en/nls.workflow.nodes.json | 5 + ui/src/i18n/locales/zh/nls.access.json | 6 ++ .../i18n/locales/zh/nls.workflow.nodes.json | 5 + 30 files changed, 586 insertions(+), 77 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/safeline/consts.go create mode 100644 internal/pkg/core/deployer/providers/safeline/safeline.go create mode 100644 internal/pkg/core/deployer/providers/safeline/safeline_test.go create mode 100644 internal/pkg/vendors/safeline-sdk/api.go create mode 100644 internal/pkg/vendors/safeline-sdk/client.go create mode 100644 ui/public/imgs/providers/safeline.svg create mode 100644 ui/src/components/access/AccessFormSafeLineConfig.tsx create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormSafeLineConfig.tsx diff --git a/README.md b/README.md index 7b735cc7..24f2859d 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ make local.run | [七牛云](https://www.qiniu.com/) | 可部署到七牛云 CDN、直播云等服务 | | [多吉云](https://www.dogecloud.com/) | 可部署到多吉云 CDN | | [优刻得](https://www.ucloud.cn/) | 可部署到优刻得 US3、UCDN 等服务 | +| [雷池](https://waf-ce.chaitin.cn/) | 可部署到雷池 WAF | | [宝塔面板](https://www.bt.cn/) | 可部署到宝塔面板 | | [AWS](https://aws.amazon.com/) | 可部署到 AWS CloudFront 等服务 | | [BytePlus](https://www.byteplus.com/) | 可部署到 BytePlus CDN 等服务 | diff --git a/README_EN.md b/README_EN.md index 580cfd34..dcb69efd 100644 --- a/README_EN.md +++ b/README_EN.md @@ -128,8 +128,9 @@ The following hosting providers are supported: | [Volcengine](https://www.volcengine.com/) | Supports deployment to Volcengine TOS, CDN, DCDN, CLB, ImageX, Live | | [Qiniu Cloud](https://www.qiniu.com/) | Supports deployment to Qiniu Cloud CDN, Pili | | [Doge Cloud](https://www.dogecloud.com/) | Supports deployment to Doge Cloud CDN | -| [BaoTa Panel](https://www.bt.cn/) | Supports deployment to BaoTa Panel sites | | [UCloud](https://www.ucloud-global.com/) | Supports deployment to UCloud US3, UCDN | +| [SafeLine](https://waf.chaitin.com/) | Supports deployment to SafeLine WAF | +| [BaoTa Panel](https://www.bt.cn/) | Supports deployment to BaoTa Panel sites | | [AWS](https://aws.amazon.com/) | Supports deployment to AWS CloudFront | | [BytePlus](https://www.byteplus.com/) | Supports deployment to BytePlus CDN | | [Edgio](https://edg.io/) | Supports deployment to Edgio Applications | diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 9b272815..4c6acc22 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -30,7 +30,8 @@ import ( pLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local" pQiniuCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn" pQiniuPili "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-pili" - providerSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh" + pSafeLine "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/safeline" + pSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh" pTencentCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn" pTencentCloudCLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-clb" pTencentCloudCOS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cos" @@ -405,6 +406,22 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, } } + case domain.DeployProviderTypeSafeLine: + { + access := domain.AccessConfigForSafeLine{} + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + deployer, err := pSafeLine.NewWithLogger(&pSafeLine.SafeLineDeployerConfig{ + ApiUrl: access.ApiUrl, + ApiToken: access.ApiToken, + ResourceType: pSafeLine.ResourceType(maps.GetValueAsString(options.ProviderDeployConfig, "resourceType")), + CertificateId: maps.GetValueAsInt32(options.ProviderDeployConfig, "certificateId"), + }, logger) + return deployer, logger, err + } + case domain.DeployProviderTypeSSH: { access := domain.AccessConfigForSSH{} @@ -412,7 +429,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } - deployer, err := providerSSH.NewWithLogger(&providerSSH.SshDeployerConfig{ + deployer, err := pSSH.NewWithLogger(&pSSH.SshDeployerConfig{ SshHost: access.Host, SshPort: access.Port, SshUsername: access.Username, @@ -422,7 +439,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, UseSCP: maps.GetValueAsBool(options.ProviderDeployConfig, "useSCP"), PreCommand: maps.GetValueAsString(options.ProviderDeployConfig, "preCommand"), PostCommand: maps.GetValueAsString(options.ProviderDeployConfig, "postCommand"), - OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "format", string(providerSSH.OUTPUT_FORMAT_PEM))), + OutputFormat: pSSH.OutputFormatType(maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "format", string(pSSH.OUTPUT_FORMAT_PEM))), OutputCertPath: maps.GetValueAsString(options.ProviderDeployConfig, "certPath"), OutputKeyPath: maps.GetValueAsString(options.ProviderDeployConfig, "keyPath"), PfxPassword: maps.GetValueAsString(options.ProviderDeployConfig, "pfxPassword"), diff --git a/internal/domain/access.go b/internal/domain/access.go index e9df61d2..ec2e093e 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -130,6 +130,11 @@ type AccessConfigForRainYun struct { ApiKey string `json:"apiKey"` } +type AccessConfigForSafeLine struct { + ApiUrl string `json:"apiUrl"` + ApiToken string `json:"apiToken"` +} + type AccessConfigForSSH struct { Host string `json:"host"` Port int32 `json:"port"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 4b77b45d..08ee9836 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -42,7 +42,7 @@ const ( AccessProviderTypePowerDNS = AccessProviderType("powerdns") AccessProviderTypeQiniu = AccessProviderType("qiniu") AccessProviderTypeRainYun = AccessProviderType("rainyun") - AccessProviderTypeSafeLine = AccessProviderType("safeline") // 雷池(预留) + AccessProviderTypeSafeLine = AccessProviderType("safeline") AccessProviderTypeSSH = AccessProviderType("ssh") AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud") AccessProviderTypeUCloud = AccessProviderType("ucloud") @@ -119,6 +119,7 @@ const ( DeployProviderTypeLocal = DeployProviderType("local") DeployProviderTypeQiniuCDN = DeployProviderType("qiniu-cdn") DeployProviderTypeQiniuPili = DeployProviderType("qiniu-pili") + DeployProviderTypeSafeLine = DeployProviderType("safeline") DeployProviderTypeSSH = DeployProviderType("ssh") DeployProviderTypeTencentCloudCDN = DeployProviderType("tencentcloud-cdn") DeployProviderTypeTencentCloudCLB = DeployProviderType("tencentcloud-clb") diff --git a/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go b/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go index 80ac4fe6..8c0ff029 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go @@ -89,10 +89,10 @@ func (d *HuaweiCloudWAFDeployer) Deploy(ctx context.Context, certPem string, pri upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem) if err != nil { return nil, xerrors.Wrap(err, "failed to upload certificate file") + } else { + d.logger.Logt("certificate file uploaded", upres) } - d.logger.Logt("certificate file uploaded", upres) - // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_CERTIFICATE: diff --git a/internal/pkg/core/deployer/providers/safeline/consts.go b/internal/pkg/core/deployer/providers/safeline/consts.go new file mode 100644 index 00000000..a19e3866 --- /dev/null +++ b/internal/pkg/core/deployer/providers/safeline/consts.go @@ -0,0 +1,8 @@ +package safeline + +type ResourceType string + +const ( + // 资源类型:替换指定证书。 + RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate") +) diff --git a/internal/pkg/core/deployer/providers/safeline/safeline.go b/internal/pkg/core/deployer/providers/safeline/safeline.go new file mode 100644 index 00000000..b759981a --- /dev/null +++ b/internal/pkg/core/deployer/providers/safeline/safeline.go @@ -0,0 +1,102 @@ +package safeline + +import ( + "context" + "errors" + "fmt" + + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/core/logger" + safelinesdk "github.com/usual2970/certimate/internal/pkg/vendors/safeline-sdk" +) + +type SafeLineDeployerConfig struct { + // 雷池 URL。 + ApiUrl string `json:"apiUrl"` + // 雷池 API Token。 + ApiToken string `json:"apiToken"` + // 部署资源类型。 + ResourceType ResourceType `json:"resourceType"` + // 证书 ID。 + // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 + CertificateId int32 `json:"certificateId,omitempty"` +} + +type SafeLineDeployer struct { + config *SafeLineDeployerConfig + logger logger.Logger + sdkClient *safelinesdk.SafeLineClient +} + +var _ deployer.Deployer = (*SafeLineDeployer)(nil) + +func New(config *SafeLineDeployerConfig) (*SafeLineDeployer, error) { + return NewWithLogger(config, logger.NewNilLogger()) +} + +func NewWithLogger(config *SafeLineDeployerConfig, logger logger.Logger) (*SafeLineDeployer, error) { + if config == nil { + return nil, errors.New("config is nil") + } + + if logger == nil { + return nil, errors.New("logger is nil") + } + + client, err := createSdkClient(config.ApiUrl, config.ApiToken) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk clients") + } + + return &SafeLineDeployer{ + logger: logger, + config: config, + sdkClient: client, + }, nil +} + +func (d *SafeLineDeployer) 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 *SafeLineDeployer) deployToCertificate(ctx context.Context, certPem string, privkeyPem string) error { + if d.config.CertificateId == 0 { + return errors.New("config `certificateId` is required") + } + + // 更新证书 + updateCertificateReq := &safelinesdk.UpdateCertificateRequest{ + Id: d.config.CertificateId, + Type: 2, + Manual: &safelinesdk.UpdateCertificateRequestBodyManul{ + Crt: certPem, + Key: privkeyPem, + }, + } + updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'safeline.UpdateCertificate'") + } else { + d.logger.Logt("已更新证书", updateCertificateResp) + } + + return nil +} + +func createSdkClient(apiUrl, apiToken string) (*safelinesdk.SafeLineClient, error) { + client := safelinesdk.NewSafeLineClient(apiUrl, apiToken) + return client, nil +} diff --git a/internal/pkg/core/deployer/providers/safeline/safeline_test.go b/internal/pkg/core/deployer/providers/safeline/safeline_test.go new file mode 100644 index 00000000..052b0aab --- /dev/null +++ b/internal/pkg/core/deployer/providers/safeline/safeline_test.go @@ -0,0 +1,76 @@ +package safeline_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/safeline" +) + +var ( + fInputCertPath string + fInputKeyPath string + fApiUrl string + fApiToken string + fCertificateId int +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_SAFELINE_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "") + flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") + flag.IntVar(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") +} + +/* +Shell command to run this test: + + go test -v ./safeline_test.go -args \ + --CERTIMATE_DEPLOYER_SAFELINE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_SAFELINE_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_SAFELINE_APIURL="your-safeline-url" \ + --CERTIMATE_DEPLOYER_SAFELINE_APITOKEN="your-safeline-api-token" \ + --CERTIMATE_DEPLOYER_SAFELINE_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("APITOKEN: %v", fApiToken), + fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), + }, "\n")) + + deployer, err := provider.New(&provider.SafeLineDeployerConfig{ + ApiUrl: fApiUrl, + ApiToken: fApiToken, + ResourceType: provider.ResourceType("certificate"), + CertificateId: 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/vendors/btpanel-sdk/client.go b/internal/pkg/vendors/btpanel-sdk/client.go index 76540ad7..047c9c54 100644 --- a/internal/pkg/vendors/btpanel-sdk/client.go +++ b/internal/pkg/vendors/btpanel-sdk/client.go @@ -9,8 +9,6 @@ import ( "time" "github.com/go-resty/resty/v2" - - "github.com/usual2970/certimate/internal/pkg/utils/maps" ) type BaoTaPanelClient struct { @@ -113,11 +111,7 @@ func (c *BaoTaPanelClient) sendRequestWithResult(path string, params map[string] return err } - jsonResp := make(map[string]any) - if err := json.Unmarshal(resp.Body(), &jsonResp); err != nil { - return fmt.Errorf("baota: failed to parse response: %w", err) - } - if err := maps.Populate(jsonResp, &result); err != nil { + if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("baota: failed to parse response: %w", err) } diff --git a/internal/pkg/vendors/gname-sdk/api.go b/internal/pkg/vendors/gname-sdk/api.go index 33972adc..282439d2 100644 --- a/internal/pkg/vendors/gname-sdk/api.go +++ b/internal/pkg/vendors/gname-sdk/api.go @@ -5,6 +5,19 @@ type BaseResponse interface { GetMsg() string } +type baseResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +func (r *baseResponse) GetCode() int { + return r.Code +} + +func (r *baseResponse) GetMsg() string { + return r.Msg +} + type AddDomainResolutionRequest struct { ZoneName string `json:"ym"` RecordType string `json:"lx"` @@ -15,17 +28,8 @@ type AddDomainResolutionRequest struct { } type AddDomainResolutionResponse struct { - Code int `json:"code"` - Msg string `json:"msg"` - Data int `json:"data"` -} - -func (r *AddDomainResolutionResponse) GetCode() int { - return r.Code -} - -func (r *AddDomainResolutionResponse) GetMsg() string { - return r.Msg + baseResponse + Data int `json:"data"` } type ModifyDomainResolutionRequest struct { @@ -39,16 +43,7 @@ type ModifyDomainResolutionRequest struct { } type ModifyDomainResolutionResponse struct { - Code int `json:"code"` - Msg string `json:"msg"` -} - -func (r *ModifyDomainResolutionResponse) GetCode() int { - return r.Code -} - -func (r *ModifyDomainResolutionResponse) GetMsg() string { - return r.Msg + baseResponse } type DeleteDomainResolutionRequest struct { @@ -57,16 +52,7 @@ type DeleteDomainResolutionRequest struct { } type DeleteDomainResolutionResponse struct { - Code int `json:"code"` - Msg string `json:"msg"` -} - -func (r *DeleteDomainResolutionResponse) GetCode() int { - return r.Code -} - -func (r *DeleteDomainResolutionResponse) GetMsg() string { - return r.Msg + baseResponse } type ListDomainResolutionRequest struct { @@ -76,8 +62,7 @@ type ListDomainResolutionRequest struct { } type ListDomainResolutionResponse struct { - Code int `json:"code"` - Msg string `json:"msg"` + baseResponse Count int `json:"count"` Data []*ResolutionRecord `json:"data"` Page int `json:"page"` @@ -92,11 +77,3 @@ type ResolutionRecord struct { RecordValue string `json:"jxz"` MX int `json:"mx"` } - -func (r *ListDomainResolutionResponse) GetCode() int { - return r.Code -} - -func (r *ListDomainResolutionResponse) GetMsg() string { - return r.Msg -} diff --git a/internal/pkg/vendors/gname-sdk/client.go b/internal/pkg/vendors/gname-sdk/client.go index 64baab95..66c2b995 100644 --- a/internal/pkg/vendors/gname-sdk/client.go +++ b/internal/pkg/vendors/gname-sdk/client.go @@ -10,8 +10,6 @@ import ( "time" "github.com/go-resty/resty/v2" - - "github.com/usual2970/certimate/internal/pkg/utils/maps" ) type GnameClient struct { @@ -131,9 +129,7 @@ func (c *GnameClient) sendRequest(path string, params map[string]any) (*resty.Re resp, err := req.Post(url) if err != nil { return nil, fmt.Errorf("gname: failed to send request: %w", err) - } - - if resp.IsError() { + } else if resp.IsError() { return nil, fmt.Errorf("gname: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) } @@ -146,11 +142,7 @@ func (c *GnameClient) sendRequestWithResult(path string, params map[string]any, return err } - jsonResp := make(map[string]any) - if err := json.Unmarshal(resp.Body(), &jsonResp); err != nil { - return fmt.Errorf("gname: failed to parse response: %w", err) - } - if err := maps.Populate(jsonResp, &result); err != nil { + if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("gname: failed to parse response: %w", err) } diff --git a/internal/pkg/vendors/safeline-sdk/api.go b/internal/pkg/vendors/safeline-sdk/api.go new file mode 100644 index 00000000..9fbfb7c9 --- /dev/null +++ b/internal/pkg/vendors/safeline-sdk/api.go @@ -0,0 +1,34 @@ +package safelinesdk + +type BaseResponse interface { + GetErrCode() *string + GetErrMsg() *string +} + +type baseResponse struct { + ErrCode *string `json:"err,omitempty"` + ErrMsg *string `json:"msg,omitempty"` +} + +func (r *baseResponse) GetErrCode() *string { + return r.ErrCode +} + +func (r *baseResponse) GetErrMsg() *string { + return r.ErrMsg +} + +type UpdateCertificateRequest struct { + Id int32 `json:"id"` + Type int32 `json:"type"` + Manual *UpdateCertificateRequestBodyManul `json:"manual"` +} + +type UpdateCertificateRequestBodyManul struct { + Crt string `json:"crt"` + Key string `json:"key"` +} + +type UpdateCertificateResponse struct { + baseResponse +} diff --git a/internal/pkg/vendors/safeline-sdk/client.go b/internal/pkg/vendors/safeline-sdk/client.go new file mode 100644 index 00000000..45ec7f51 --- /dev/null +++ b/internal/pkg/vendors/safeline-sdk/client.go @@ -0,0 +1,87 @@ +package safelinesdk + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/go-resty/resty/v2" +) + +type SafeLineClient struct { + apiHost string + apiToken string + client *resty.Client +} + +func NewSafeLineClient(apiHost, apiToken string) *SafeLineClient { + client := resty.New() + + return &SafeLineClient{ + apiHost: apiHost, + apiToken: apiToken, + client: client, + } +} + +func (c *SafeLineClient) WithTimeout(timeout time.Duration) *SafeLineClient { + c.client.SetTimeout(timeout) + return c +} + +func (c *SafeLineClient) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { + params := make(map[string]any) + jsonData, _ := json.Marshal(req) + json.Unmarshal(jsonData, ¶ms) + + result := UpdateCertificateResponse{} + err := c.sendRequestWithResult("/api/open/cert", params, &result) + if err != nil { + return nil, err + } + return &result, nil +} + +func (c *SafeLineClient) sendRequest(path string, params map[string]any) (*resty.Response, error) { + if params == nil { + params = make(map[string]any) + } + + url := strings.TrimRight(c.apiHost, "/") + path + req := c.client.R(). + SetHeader("Content-Type", "application/json"). + SetHeader("X-SLCE-API-TOKEN", c.apiToken). + SetBody(params) + resp, err := req.Post(url) + if err != nil { + return nil, fmt.Errorf("safeline: failed to send request: %w", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("safeline: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + } + + return resp, nil +} + +func (c *SafeLineClient) sendRequestWithResult(path string, params map[string]any, result BaseResponse) error { + resp, err := c.sendRequest(path, params) + if err != nil { + return err + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return fmt.Errorf("safeline: failed to parse response: %w", err) + } + + if result.GetErrCode() != nil && *result.GetErrCode() != "" { + if result.GetErrMsg() == nil { + return fmt.Errorf("safeline api error: %s", *result.GetErrCode()) + } else { + return fmt.Errorf("safeline api error: %s, %s", *result.GetErrCode(), *result.GetErrMsg()) + } + } + + return nil +} diff --git a/ui/public/imgs/providers/safeline.svg b/ui/public/imgs/providers/safeline.svg new file mode 100644 index 00000000..b5490ac7 --- /dev/null +++ b/ui/public/imgs/providers/safeline.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index d9632f28..71aa3fba 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -31,6 +31,7 @@ import AccessFormNS1Config from "./AccessFormNS1Config"; import AccessFormPowerDNSConfig from "./AccessFormPowerDNSConfig"; import AccessFormQiniuConfig from "./AccessFormQiniuConfig"; import AccessFormRainYunConfig from "./AccessFormRainYunConfig"; +import AccessFormSafeLineConfig from "./AccessFormSafeLineConfig"; import AccessFormSSHConfig from "./AccessFormSSHConfig"; import AccessFormTencentCloudConfig from "./AccessFormTencentCloudConfig"; import AccessFormUCloudConfig from "./AccessFormUCloudConfig"; @@ -134,6 +135,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.RAINYUN: return ; + case ACCESS_PROVIDERS.SAFELINE: + return ; case ACCESS_PROVIDERS.SSH: return ; case ACCESS_PROVIDERS.TENCENTCLOUD: diff --git a/ui/src/components/access/AccessFormBaotaPanelConfig.tsx b/ui/src/components/access/AccessFormBaotaPanelConfig.tsx index 4f619c1c..accd80d0 100644 --- a/ui/src/components/access/AccessFormBaotaPanelConfig.tsx +++ b/ui/src/components/access/AccessFormBaotaPanelConfig.tsx @@ -17,7 +17,7 @@ export type AccessFormBaotaPanelConfigProps = { const initFormModel = (): AccessFormBaotaPanelConfigFieldValues => { return { - apiUrl: "", + apiUrl: "http://:8888/", apiKey: "", }; }; diff --git a/ui/src/components/access/AccessFormPowerDNSConfig.tsx b/ui/src/components/access/AccessFormPowerDNSConfig.tsx index 83a22b9b..e93980c7 100644 --- a/ui/src/components/access/AccessFormPowerDNSConfig.tsx +++ b/ui/src/components/access/AccessFormPowerDNSConfig.tsx @@ -17,7 +17,7 @@ export type AccessFormPowerDNSConfigProps = { const initFormModel = (): AccessFormPowerDNSConfigFieldValues => { return { - apiUrl: "", + apiUrl: "http://:8082/", apiKey: "", }; }; diff --git a/ui/src/components/access/AccessFormSafeLineConfig.tsx b/ui/src/components/access/AccessFormSafeLineConfig.tsx new file mode 100644 index 00000000..b9de115c --- /dev/null +++ b/ui/src/components/access/AccessFormSafeLineConfig.tsx @@ -0,0 +1,72 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForSafeLine } from "@/domain/access"; + +type AccessFormSafeLineConfigFieldValues = Nullish; + +export type AccessFormSafeLineConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormSafeLineConfigFieldValues; + onValuesChange?: (values: AccessFormSafeLineConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormSafeLineConfigFieldValues => { + return { + apiUrl: "http://:9443/", + apiToken: "", + }; +}; + +const AccessFormSafeLineConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormSafeLineConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiUrl: z.string().url(t("common.errmsg.url_invalid")), + apiToken: z + .string() + .min(1, t("access.form.safeline_api_token.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 AccessFormSafeLineConfig; diff --git a/ui/src/components/provider/AccessProviderSelect.tsx b/ui/src/components/provider/AccessProviderSelect.tsx index 04b859ac..97cd35fd 100644 --- a/ui/src/components/provider/AccessProviderSelect.tsx +++ b/ui/src/components/provider/AccessProviderSelect.tsx @@ -47,12 +47,18 @@ const AccessProviderSelect = (props: AccessProviderSelectProps) => { return ( { + if (!option) return false; + + const value = inputValue.toLowerCase(); + return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); + }} labelRender={({ label, value }) => { - if (label) { - return renderOption(value as string); + if (!label) { + return {props.placeholder}; } - return {props.placeholder}; + return renderOption(value as string); }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/provider/DeployProviderSelect.tsx b/ui/src/components/provider/DeployProviderSelect.tsx index 54e3643b..cc2b13ea 100644 --- a/ui/src/components/provider/DeployProviderSelect.tsx +++ b/ui/src/components/provider/DeployProviderSelect.tsx @@ -33,12 +33,18 @@ const DeployProviderSelect = (props: DeployProviderSelectProps) => { return ( + + {t("workflow_node.deploy.form.safeline_resource_type.option.certificate.label")} + + + + + + + + + + + ); +}; + +export default DeployNodeConfigFormSafeLineConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index cf6b1055..d8a64dd4 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -27,6 +27,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForPowerDNS | AccessConfigForQiniu | AccessConfigForRainYun + | AccessConfigForSafeLine | AccessConfigForSSH | AccessConfigForTencentCloud | AccessConfigForUCloud @@ -143,6 +144,11 @@ export type AccessConfigForRainYun = { apiKey: string; }; +export type AccessConfigForSafeLine = { + apiUrl: string; + apiToken: string; +}; + export type AccessConfigForSSH = { host: string; port: number; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 96b69a85..d68e5210 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -26,6 +26,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ POWERDNS: "powerdns", QINIU: "qiniu", RAINYUN: "rainyun", + SAFELINE: "safeline", SSH: "ssh", TENCENTCLOUD: "tencentcloud", UCLOUD: "ucloud", @@ -70,6 +71,7 @@ export const accessProvidersMap: Map [ type, { diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index af3676b3..a3a16683 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -147,6 +147,12 @@ "access.form.rainyun_api_key.label": "Rain Yun API key", "access.form.rainyun_api_key.placeholder": "Please enter Rain Yun API key", "access.form.rainyun_api_key.tooltip": "For more information, see https://www.rainyun.com/docs/account/racc/setting", + "access.form.safeline_api_url.label": "SafeLine URL", + "access.form.safeline_api_url.placeholder": "Please enter SafeLine URL", + "access.form.safeline_api_url.tooltip": "For more information, see https://docs.waf.chaitin.com/en/tutorials/install", + "access.form.safeline_api_token.label": "SafeLine API token", + "access.form.safeline_api_token.placeholder": "Please enter SafeLine API token", + "access.form.safeline_api_token.tooltip": "For more information, see https://docs.waf.chaitin.com/en/reference/articles/openapi", "access.form.ssh_host.label": "Server host", "access.form.ssh_host.placeholder": "Please enter server host", "access.form.ssh_port.label": "Server port", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 9e226284..360a0607 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -300,6 +300,11 @@ "workflow_node.deploy.form.qiniu_pili_domain.label": "Qiniu Pili streaming domain", "workflow_node.deploy.form.qiniu_pili_domain.placeholder": "Please enter Qiniu Pili streaming domain name", "workflow_node.deploy.form.qiniu_pili_domain.tooltip": "For more information, see https://portal.qiniu.com/hub", + "workflow_node.deploy.form.safeline_resource_type.label": "Resource type", + "workflow_node.deploy.form.safeline_resource_type.placeholder": "Please select resource type", + "workflow_node.deploy.form.safeline_resource_type.option.certificate.label": "Certificate", + "workflow_node.deploy.form.safeline_certificate_id.label": "SafeLine certificate ID", + "workflow_node.deploy.form.safeline_certificate_id.placeholder": "Please enter SafeLine certificate ID", "workflow_node.deploy.form.ssh_format.label": "File format", "workflow_node.deploy.form.ssh_format.placeholder": "Please select file format", "workflow_node.deploy.form.ssh_format.option.pem.label": "PEM (*.pem, *.crt, *.key)", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 37ebb112..586af5f3 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -147,6 +147,12 @@ "access.form.rainyun_api_key.label": "雨云 API 密钥", "access.form.rainyun_api_key.placeholder": "请输入雨云 API 密钥", "access.form.rainyun_api_key.tooltip": "这是什么?请参阅 https://www.rainyun.com/docs/account/racc/setting", + "access.form.safeline_api_url.label": "雷池 URL", + "access.form.safeline_api_url.placeholder": "请输入雷池 URL", + "access.form.safeline_api_url.tooltip": "这是什么?请参阅 https://docs.waf-ce.chaitin.cn/zh/上手指南/安装雷池", + "access.form.safeline_api_token.label": "雷池 API Token", + "access.form.safeline_api_token.placeholder": "请输入雷池 API Token", + "access.form.safeline_api_token.tooltip": "这是什么?请参阅 https://docs.waf-ce.chaitin.cn/zh/更多技术文档/OPENAPI", "access.form.ssh_host.label": "服务器地址", "access.form.ssh_host.placeholder": "请输入服务器地址", "access.form.ssh_port.label": "服务器端口", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index f0c07485..7fe81657 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -300,6 +300,11 @@ "workflow_node.deploy.form.qiniu_pili_domain.label": "七牛云视频直播流域名", "workflow_node.deploy.form.qiniu_pili_domain.placeholder": "请输入七牛云视频直播流域名", "workflow_node.deploy.form.qiniu_pili_domain.tooltip": "这是什么?请参阅 https://portal.qiniu.com/hub", + "workflow_node.deploy.form.safeline_resource_type.label": "证书替换方式", + "workflow_node.deploy.form.safeline_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.safeline_resource_type.option.certificate.label": "替换指定证书", + "workflow_node.deploy.form.safeline_certificate_id.label": "雷池证书 ID", + "workflow_node.deploy.form.safeline_certificate_id.placeholder": "请输入雷池证书 ID", "workflow_node.deploy.form.ssh_format.label": "文件格式", "workflow_node.deploy.form.ssh_format.placeholder": "请选择文件格式", "workflow_node.deploy.form.ssh_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key)",