Merge branch 'usual2970:main' into main

This commit is contained in:
RHQYZ 2025-04-13 11:57:30 +08:00 committed by GitHub
commit 363e23dd13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 2487 additions and 108 deletions

3
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates v0.9.0
github.com/Edgio/edgio-api v0.0.0-workspace
github.com/G-Core/gcorelabscdn-go v1.0.28
github.com/alibabacloud-go/alb-20200616/v2 v2.2.8
github.com/alibabacloud-go/cas-20200407/v3 v3.0.4
@ -211,6 +212,8 @@ require (
modernc.org/sqlite v1.36.1 // indirect
)
replace github.com/Edgio/edgio-api v0.0.0-workspace => ./internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace
replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkcore@v1.0.0
replace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkclouddns@v1.0.1

2
go.sum
View File

@ -498,6 +498,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=

View File

@ -49,6 +49,7 @@ 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"
pRainYunRCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/rainyun-rcdn"
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"
@ -73,6 +74,7 @@ import (
pVolcEngineImageX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-imagex"
pVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live"
pVolcEngineTOS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-tos"
pWangsuCDNPro "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro"
pWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook"
"github.com/usual2970/certimate/internal/pkg/utils/maputil"
"github.com/usual2970/certimate/internal/pkg/utils/sliceutil"
@ -681,6 +683,27 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
}
}
case domain.DeployProviderTypeRainYunRCDN:
{
access := domain.AccessConfigForRainYun{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
case domain.DeployProviderTypeTencentCloudCDN:
deployer, err := pRainYunRCDN.NewDeployer(&pRainYunRCDN.DeployerConfig{
ApiKey: access.ApiKey,
InstanceId: maputil.GetInt32(options.ProviderDeployConfig, "instanceId"),
Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
})
return deployer, err
default:
break
}
}
case domain.DeployProviderTypeSafeLine:
{
access := domain.AccessConfigForSafeLine{}
@ -981,6 +1004,30 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
}
}
case domain.DeployProviderTypeWangsuCDNPro:
{
access := domain.AccessConfigForWangsu{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
case domain.DeployProviderTypeWangsuCDNPro:
deployer, err := pWangsuCDNPro.NewDeployer(&pWangsuCDNPro.DeployerConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Environment: maputil.GetOrDefaultString(options.ProviderDeployConfig, "environment", "production"),
Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
CertificateId: maputil.GetString(options.ProviderDeployConfig, "certificateId"),
WebhookId: maputil.GetString(options.ProviderDeployConfig, "webhookId"),
})
return deployer, err
default:
break
}
}
case domain.DeployProviderTypeWebhook:
{
access := domain.AccessConfigForWebhook{}

View File

@ -228,6 +228,11 @@ type AccessConfigForVolcEngine struct {
SecretAccessKey string `json:"secretAccessKey"`
}
type AccessConfigForWangsu struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type AccessConfigForWebhook struct {
Url string `json:"url"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`

View File

@ -14,6 +14,8 @@ const (
NotifyChannelTypeEmail = NotifyChannelType("email")
NotifyChannelTypeGotify = NotifyChannelType("gotify")
NotifyChannelTypeLark = NotifyChannelType("lark")
NotifyChannelTypeMattermost = NotifyChannelType("mattermost")
NotifyChannelTypePushover = NotifyChannelType("pushover")
NotifyChannelTypePushPlus = NotifyChannelType("pushplus")
NotifyChannelTypeServerChan = NotifyChannelType("serverchan")
NotifyChannelTypeTelegram = NotifyChannelType("telegram")

View File

@ -61,6 +61,7 @@ const (
AccessProviderTypeUpyun = AccessProviderType("upyun")
AccessProviderTypeVercel = AccessProviderType("vercel")
AccessProviderTypeVolcEngine = AccessProviderType("volcengine")
AccessProviderTypeWangsu = AccessProviderType("wangsu")
AccessProviderTypeWebhook = AccessProviderType("webhook")
AccessProviderTypeWestcn = AccessProviderType("westcn")
AccessProviderTypeZeroSSL = AccessProviderType("zerossl")
@ -186,6 +187,7 @@ const (
DeployProviderTypeQiniuCDN = DeployProviderType("qiniu-cdn")
DeployProviderTypeQiniuKodo = DeployProviderType("qiniu-kodo")
DeployProviderTypeQiniuPili = DeployProviderType("qiniu-pili")
DeployProviderTypeRainYunRCDN = DeployProviderType("rainyun-rcdn")
DeployProviderTypeSafeLine = DeployProviderType("safeline")
DeployProviderTypeSSH = DeployProviderType("ssh")
DeployProviderTypeTencentCloudCDN = DeployProviderType("tencentcloud-cdn")
@ -211,5 +213,6 @@ const (
DeployProviderTypeVolcEngineImageX = DeployProviderType("volcengine-imagex")
DeployProviderTypeVolcEngineLive = DeployProviderType("volcengine-live")
DeployProviderTypeVolcEngineTOS = DeployProviderType("volcengine-tos")
DeployProviderTypeWangsuCDNPro = DeployProviderType("wangsu-cdnpro")
DeployProviderTypeWebhook = DeployProviderType("webhook")
)

View File

@ -10,6 +10,8 @@ import (
pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify"
pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
pPushover "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover"
pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus"
pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
@ -59,6 +61,19 @@ func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]a
WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"),
})
case domain.NotifyChannelTypeMattermost:
return pMattermost.NewNotifier(&pMattermost.NotifierConfig{
ServerUrl: maputil.GetString(channelConfig, "serverUrl"),
ChannelId: maputil.GetString(channelConfig, "channelId"),
Username: maputil.GetString(channelConfig, "username"),
Password: maputil.GetString(channelConfig, "password"),
})
case domain.NotifyChannelTypePushover:
return pPushover.NewNotifier(&pPushover.NotifierConfig{
Token: maputil.GetString(channelConfig, "token"),
User: maputil.GetString(channelConfig, "user"),
})
case domain.NotifyChannelTypePushPlus:
return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{
Token: maputil.GetString(channelConfig, "token"),

View File

@ -4,12 +4,12 @@ import (
"context"
"log/slog"
edgio "github.com/Edgio/edgio-api/applications/v7"
edgiodtos "github.com/Edgio/edgio-api/applications/v7/dtos"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/certutil"
edgsdk "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7"
edgsdkdtos "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos"
)
type DeployerConfig struct {
@ -24,7 +24,7 @@ type DeployerConfig struct {
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *edgsdk.EdgioClient
sdkClient *edgio.EdgioClient
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
@ -64,7 +64,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
// 上传 TLS 证书
// REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts
uploadTlsCertReq := edgsdkdtos.UploadTlsCertRequest{
uploadTlsCertReq := edgiodtos.UploadTlsCertRequest{
EnvironmentID: d.config.EnvironmentId,
PrimaryCert: privateCertPem,
IntermediateCert: intermediateCertPem,
@ -79,7 +79,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
return &deployer.DeployResult{}, nil
}
func createSdkClient(clientId, clientSecret string) (*edgsdk.EdgioClient, error) {
client := edgsdk.NewEdgioClient(clientId, clientSecret, "", "")
func createSdkClient(clientId, clientSecret string) (*edgio.EdgioClient, error) {
client := edgio.NewEdgioClient(clientId, clientSecret, "", "")
return client, nil
}

View File

@ -100,9 +100,15 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
SSlEnabled: true,
SSLData: int(updateResourceCertId),
ProxySSLEnabled: getResourceResp.ProxySSLEnabled,
ProxySSLCA: &getResourceResp.ProxySSLCA,
ProxySSLData: &getResourceResp.ProxySSLData,
Options: getResourceResp.Options,
}
if getResourceResp.ProxySSLCA != 0 {
updateResourceReq.ProxySSLCA = &getResourceResp.ProxySSLCA
}
if getResourceResp.ProxySSLData != 0 {
updateResourceReq.ProxySSLData = &getResourceResp.ProxySSLData
}
if getResourceResp.Options != nil {
updateResourceReq.Options = getResourceResp.Options
}
updateResourceResp, err := d.sdkClient.Update(context.TODO(), d.config.ResourceId, updateResourceReq)
d.logger.Debug("sdk request 'resources.Update'", slog.Int64("resourceId", d.config.ResourceId), slog.Any("request", updateResourceReq), slog.Any("response", updateResourceResp))

View File

@ -0,0 +1,102 @@
package rainyunrcdn
import (
"context"
"errors"
"log/slog"
"strconv"
xerrors "github.com/pkg/errors"
"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/rainyun-sslcenter"
rainyunsdk "github.com/usual2970/certimate/internal/pkg/vendors/rainyun-sdk"
)
type DeployerConfig struct {
// 雨云 API 密钥。
ApiKey string `json:"apiKey"`
// RCDN 实例 ID。
InstanceId int32 `json:"instanceId"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *rainyunsdk.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.ApiKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
ApiKey: config.ApiKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
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) {
// 上传证书到 SSL 证书
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// RCDN SSL 绑定域名
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-184214120
certId, _ := strconv.Atoi(upres.CertId)
rcdnInstanceSslBindReq := &rainyunsdk.RcdnInstanceSslBindRequest{
CertId: int32(certId),
Domains: []string{d.config.Domain},
}
rcdnInstanceSslBindResp, err := d.sdkClient.RcdnInstanceSslBind(d.config.InstanceId, rcdnInstanceSslBindReq)
d.logger.Debug("sdk request 'rcdn.InstanceSslBind'", slog.Any("instanceId", d.config.InstanceId), slog.Any("request", rcdnInstanceSslBindReq), slog.Any("response", rcdnInstanceSslBindResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'rcdn.InstanceSslBind'")
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(apiKey string) (*rainyunsdk.Client, error) {
if apiKey == "" {
return nil, errors.New("invalid rainyun api key")
}
client := rainyunsdk.NewClient(apiKey)
return client, nil
}

View File

@ -0,0 +1,75 @@
package rainyunrcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/rainyun-rcdn"
)
var (
fInputCertPath string
fInputKeyPath string
fApiKey string
fInstanceId int64
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_RAINYUNRCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.Int64Var(&fInstanceId, argsPrefix+"INSTANCEID", 0, "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./ucloud_ucdn_test.go -args \
--CERTIMATE_DEPLOYER_RAINYUNRCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_RAINYUNRCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_RAINYUNRCDN_APIKEY="your-api-key" \
--CERTIMATE_DEPLOYER_RAINYUNRCDN_INSTANCEID="your-rcdn-instance-id" \
--CERTIMATE_DEPLOYER_RAINYUNRCDN_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("APIKEY: %v", fApiKey),
fmt.Sprintf("INSTANCEID: %v", fInstanceId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
PrivateKey: fApiKey,
InstanceId: fInstanceId,
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)
})
}

View File

@ -107,7 +107,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcssl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("ecdn")
deployCertificateInstanceReq.ResourceType = common.StringPtr("cdn")
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(instanceIds)
deployCertificateInstanceResp, err := d.sdkClients.SSL.DeployCertificateInstance(deployCertificateInstanceReq)

View File

@ -182,7 +182,7 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str
return errors.New("config `listenerId` is required")
}
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, cloudCertId); err != nil {
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
return err
}

View File

@ -178,7 +178,7 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str
return errors.New("config `listenerId` is required")
}
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, cloudCertId); err != nil {
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
return err
}

View File

@ -0,0 +1,276 @@
package wangsucdnpro
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"log/slog"
"regexp"
"time"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/certutil"
wangsucdn "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/cdn"
)
type DeployerConfig struct {
// 网宿云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 网宿云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 网宿云环境。
Environment string `json:"environment"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
// 证书 ID。
// 选填。
CertificateId string `json:"certificateId,omitempty"`
// Webhook ID。
// 选填。
WebhookId string `json:"webhookId,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *wangsucdn.Client
}
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)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
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) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询已部署加速域名的详情
getHostnameDetailResp, err := d.sdkClient.GetHostnameDetail(d.config.Domain)
d.logger.Debug("sdk request 'cdn.GetHostnameDetail'", slog.String("hostname", d.config.Domain), slog.Any("response", getHostnameDetailResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetHostnameDetail'")
}
// 生成网宿云证书参数
encryptedPrivateKey, err := encryptPrivateKey(privkeyPem, d.config.AccessKeySecret, time.Now().Unix())
if err != nil {
return nil, xerrors.Wrap(err, "failed to encrypt private key")
}
certificateNewVersionInfo := &wangsucdn.CertificateVersion{
PrivateKey: tea.String(encryptedPrivateKey),
Certificate: tea.String(certPem),
IdentificationInfo: &wangsucdn.CertificateVersionIdentificationInfo{
CommonName: tea.String(certX509.Subject.CommonName),
SubjectAlternativeNames: &certX509.DNSNames,
},
}
// 网宿云证书 URL 中包含证书 ID 及版本号
// 格式:
// http://open.chinanetcenter.com/cdn/certificates/5dca2205f9e9cc0001df7b33
// http://open.chinanetcenter.com/cdn/certificates/329f12c1fe6708c23c31e91f/versions/5
var wangsuCertUrl string
var wangsuCertId, wangsuCertVer string
// 如果原证书 ID 为空,则创建证书;否则更新证书。
timestamp := time.Now().Unix()
if d.config.CertificateId == "" {
// 创建证书
createCertificateReq := &wangsucdn.CreateCertificateRequest{
Timestamp: timestamp,
Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
AutoRenew: tea.String("Off"),
NewVersion: certificateNewVersionInfo,
}
createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateCertificate'")
}
wangsuCertUrl = createCertificateResp.CertificateUrl
d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl))
wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
if len(wangsuCertIdMatches) > 1 {
wangsuCertId = wangsuCertIdMatches[1]
}
wangsuCertVer = "1"
} else {
// 更新证书
updateCertificateReq := &wangsucdn.UpdateCertificateRequest{
Timestamp: timestamp,
Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
AutoRenew: tea.String("Off"),
NewVersion: certificateNewVersionInfo,
}
updateCertificateResp, err := d.sdkClient.UpdateCertificate(d.config.CertificateId, updateCertificateReq)
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("certificateId", d.config.CertificateId), slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UpdateCertificate'")
}
wangsuCertUrl = updateCertificateResp.CertificateUrl
d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl))
wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
if len(wangsuCertIdMatches) > 1 {
wangsuCertId = wangsuCertIdMatches[1]
}
wangsuCertVerMatches := regexp.MustCompile(`/versions/(\d+)`).FindStringSubmatch(wangsuCertUrl)
if len(wangsuCertVerMatches) > 1 {
wangsuCertVer = wangsuCertVerMatches[1]
}
}
// 创建部署任务
// REF: https://www.wangsu.com/document/api-doc/27034
createDeploymentTaskReq := &wangsucdn.CreateDeploymentTaskRequest{
Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
Target: tea.String(d.config.Environment),
Actions: &[]wangsucdn.DeploymentTaskAction{
{
Action: tea.String("deploy_cert"),
CertificateId: tea.String(wangsuCertId),
Version: tea.String(wangsuCertVer),
},
},
}
if d.config.WebhookId != "" {
createDeploymentTaskReq.Webhook = tea.String(d.config.WebhookId)
}
createDeploymentTaskResp, err := d.sdkClient.CreateDeploymentTask(createDeploymentTaskReq)
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createDeploymentTaskReq), slog.Any("response", createDeploymentTaskResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateDeploymentTask'")
}
// 循环获取部署任务详细信息,等待任务状态变更
// REF: https://www.wangsu.com/document/api-doc/27038
var wangsuTaskId string
wangsuTaskMatches := regexp.MustCompile(`/deploymentTasks/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
if len(wangsuTaskMatches) > 1 {
wangsuTaskId = wangsuTaskMatches[1]
}
for {
if ctx.Err() != nil {
return nil, ctx.Err()
}
getDeploymentTaskDetailResp, err := d.sdkClient.GetDeploymentTaskDetail(wangsuTaskId)
d.logger.Debug("sdk request 'cdn.GetDeploymentTaskDetail'", slog.Any("taskId", wangsuTaskId), slog.Any("response", getDeploymentTaskDetailResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDeploymentTaskDetail'")
}
if getDeploymentTaskDetailResp.Status == "failed" {
return nil, errors.New("unexpected deployment task status")
} else if getDeploymentTaskDetailResp.Status == "succeeded" {
break
}
d.logger.Info("waiting for deployment task completion ...")
time.Sleep(time.Second * 15)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, accessKeySecret string) (*wangsucdn.Client, error) {
if accessKeyId == "" {
return nil, errors.New("invalid wangsu access key id")
}
if accessKeySecret == "" {
return nil, errors.New("invalid wangsu access key secret")
}
return wangsucdn.NewClient(accessKeyId, accessKeySecret), nil
}
func encryptPrivateKey(privkeyPem string, secretKey string, timestamp int64) (string, error) {
date := time.Unix(timestamp, 0).UTC()
dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 GMT")
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(dateStr))
aesivkey := mac.Sum(nil)
aesivkeyHex := hex.EncodeToString(aesivkey)
if len(aesivkeyHex) != 64 {
return "", fmt.Errorf("invalid hmac length: %d", len(aesivkeyHex))
}
ivHex := aesivkeyHex[:32]
keyHex := aesivkeyHex[32:64]
iv, err := hex.DecodeString(ivHex)
if err != nil {
return "", fmt.Errorf("failed to decode iv: %w", err)
}
key, err := hex.DecodeString(keyHex)
if err != nil {
return "", fmt.Errorf("failed to decode key: %w", err)
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
plainBytes := []byte(privkeyPem)
padlen := aes.BlockSize - len(plainBytes)%aes.BlockSize
if padlen > 0 {
paddata := bytes.Repeat([]byte{byte(padlen)}, padlen)
plainBytes = append(plainBytes, paddata...)
}
encBytes := make([]byte, len(plainBytes))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(encBytes, plainBytes)
return base64.StdEncoding.EncodeToString(encBytes), nil
}

View File

@ -0,0 +1,90 @@
package wangsucdnpro_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fEnvironment string
fDomain string
fCertificateId string
fWebhookId string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_WANGSUCDNPRO_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fEnvironment, argsPrefix+"ENVIRONMENT", "production", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
flag.StringVar(&fWebhookId, argsPrefix+"WEBHOOKID", "", "")
}
/*
Shell command to run this test:
go test -v ./wangsu_cdnpro_test.go -args \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_ENVIRONMENT="production" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_DOMAIN="example.com" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_CERTIFICATEID="your-certificate-id"\
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_WEBHOOKID="your-webhook-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("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("ENVIRONMENT: %v", fEnvironment),
fmt.Sprintf("DOMAIN: %v", fDomain),
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
fmt.Sprintf("WEBHOOKID: %v", fWebhookId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Environment: fEnvironment,
Domain: fDomain,
CertificateId: fCertificateId,
WebhookId: fWebhookId,
})
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)
})
}

View File

@ -0,0 +1,89 @@
package mattermost
import (
"bytes"
"context"
"encoding/json"
"github.com/nikoksr/notify/service/mattermost"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
"io"
"log/slog"
"net/http"
)
type NotifierConfig struct {
// Mattermost 服务地址。
ServerUrl string `json:"serverUrl"`
// 频道ID
ChannelId string `json:"channelId"`
// 用户名
Username string `json:"username"`
// 密码
Password string `json:"password"`
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
if config == nil {
panic("config is nil")
}
return &NotifierProvider{
config: config,
}, nil
}
func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
if logger == nil {
n.logger = slog.Default()
} else {
n.logger = logger
}
return n
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := mattermost.New(n.config.ServerUrl)
if err := srv.LoginWithCredentials(ctx, n.config.Username, n.config.Password); err != nil {
return nil, err
}
srv.AddReceivers(n.config.ChannelId)
// 复写消息样式
srv.PreSend(func(req *http.Request) error {
m := map[string]interface{}{
"channel_id": n.config.ChannelId,
"props": map[string]interface{}{
"attachments": []map[string]interface{}{
{
"title": subject,
"text": message,
},
},
},
}
if body, err := json.Marshal(m); err != nil {
return err
} else {
req.ContentLength = int64(len(body))
req.Body = io.NopCloser(bytes.NewReader(body))
}
return nil
})
if err = srv.Send(ctx, subject, message); err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@ -0,0 +1,74 @@
package mattermost_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fServerUrl string
fChannelId string
fUsername string
fPassword string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_MATTERMOST_"
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "")
flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "")
flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "")
}
/*
Shell command to run this test:
go test -v ./mattermost_test.go -args \
--CERTIMATE_NOTIFIER_MATTERMOST_SERVERURL="https://example.com/your-server-url" \
--CERTIMATE_NOTIFIER_MATTERMOST_CHANNELID="your-chanel-id" \
--CERTIMATE_NOTIFIER_MATTERMOST_USERNAME="your-username" \
--CERTIMATE_NOTIFIER_MATTERMOST_PASSWORD="your-password"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("SERVERURL: %v", fServerUrl),
fmt.Sprintf("CHANNELID: %v", fChannelId),
fmt.Sprintf("USERNAME: %v", fUsername),
fmt.Sprintf("PASSWORD: %v", fPassword),
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
ServerUrl: fServerUrl,
ChannelId: fChannelId,
Username: fUsername,
Password: fPassword,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,102 @@
package pushover
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
Token string `json:"token"` // 应用 API Token
User string `json:"user"` // 用户/分组 Key
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
// 未来将移除
httpClient *http.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
if config == nil {
panic("config is nil")
}
return &NotifierProvider{
config: config,
httpClient: http.DefaultClient,
}, nil
}
func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
if logger == nil {
n.logger = slog.Default()
} else {
n.logger = logger
}
return n
}
// Notify 发送通知
// 参考文档https://pushover.net/api
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
// 请求体
reqBody := &struct {
Token string `json:"token"`
User string `json:"user"`
Title string `json:"title"`
Message string `json:"message"`
}{
Token: n.config.Token,
User: n.config.User,
Title: subject,
Message: message,
}
// Make request
body, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.Wrap(err, "encode message body")
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
"https://api.pushover.net/1/messages.json",
bytes.NewReader(body),
)
if err != nil {
return nil, errors.Wrap(err, "create new request")
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
// Send request to pushover service
resp, err := n.httpClient.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "send request to pushover server")
}
defer resp.Body.Close()
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "read response")
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("pushover returned status code %d: %s", resp.StatusCode, string(result))
}
return &notifier.NotifyResult{}, nil
}

View File

@ -0,0 +1,62 @@
package pushover_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fToken string
fUser string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_PUSHOVER_"
flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "")
flag.StringVar(&fUser, argsPrefix+"USER", "", "")
}
/*
Shell command to run this test:
go test -v ./pushover_test.go -args \
--CERTIMATE_NOTIFIER_PUSHOVER_TOKEN="your-pushover-token" \
--CERTIMATE_NOTIFIER_PUSHOVER_USER="your-pushover-user" \
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("TOKEN: %v", fToken),
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
Token: fToken,
User: fUser,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -58,7 +58,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 遍历证书列表,避免重复上传
if res, err := u.getExistCert(ctx, certPem, privkeyPem); err != nil {
if res, err := u.getCertIfExists(ctx, certPem, privkeyPem); err != nil {
return nil, err
} else if res != nil {
u.logger.Info("ssl certificate already exists")
@ -82,7 +82,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
}
// 遍历证书列表,获取刚刚上传证书 ID
if res, err := u.getExistCert(ctx, certPem, privkeyPem); err != nil {
if res, err := u.getCertIfExists(ctx, certPem, privkeyPem); err != nil {
return nil, err
} else if res == nil {
return nil, fmt.Errorf("no ssl certificate found, may be upload failed (code: %d, message: %s)", uploadWebsiteSSLResp.GetCode(), uploadWebsiteSSLResp.GetMessage())
@ -91,7 +91,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
}
}
func (u *UploaderProvider) getExistCert(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
searchWebsiteSSLPageNumber := int32(1)
searchWebsiteSSLPageSize := int32(100)
for {

View File

@ -0,0 +1,169 @@
package rainyunsslcenter
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/certutil"
rainyunsdk "github.com/usual2970/certimate/internal/pkg/vendors/rainyun-sdk"
)
type UploaderConfig struct {
// 雨云 API 密钥。
ApiKey string `json:"ApiKey"`
}
type UploaderProvider struct {
config *UploaderConfig
logger *slog.Logger
sdkClient *rainyunsdk.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &UploaderProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
if logger == nil {
u.logger = slog.Default()
} else {
u.logger = logger
}
return u
}
func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
if res, err := u.getCertIfExists(ctx, certPem); err != nil {
return nil, err
} else if res != nil {
u.logger.Info("ssl certificate already exists")
return res, nil
}
// SSL 证书上传
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046
sslCenterCreateReq := &rainyunsdk.SslCenterCreateRequest{
Cert: certPem,
Key: privkeyPem,
}
sslCenterCreateResp, err := u.sdkClient.SslCenterCreate(sslCenterCreateReq)
u.logger.Debug("sdk request 'sslcenter.Create'", slog.Any("request", sslCenterCreateReq), slog.Any("response", sslCenterCreateResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.Create'")
}
if res, err := u.getCertIfExists(ctx, certPem); err != nil {
return nil, err
} else if res == nil {
return nil, errors.New("rainyun sslcenter: no certificate found")
} else {
return res, nil
}
}
func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 遍历 SSL 证书列表,避免重复上传
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943048
sslCenterListPage := int32(1)
sslCenterListPerPage := int32(100)
for {
sslCenterListReq := &rainyunsdk.SslCenterListRequest{
Filters: &rainyunsdk.SslCenterListFilters{
Domain: &certX509.Subject.CommonName,
},
Page: &sslCenterListPage,
PerPage: &sslCenterListPerPage,
}
sslCenterListResp, err := u.sdkClient.SslCenterList(sslCenterListReq)
u.logger.Debug("sdk request 'sslcenter.List'", slog.Any("request", sslCenterListReq), slog.Any("response", sslCenterListResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.List'")
}
if sslCenterListResp.Data != nil && sslCenterListResp.Data.Records != nil {
for _, sslItem := range sslCenterListResp.Data.Records {
// 先对比证书的多域名
if sslItem.Domain != strings.Join(certX509.DNSNames, ", ") {
continue
}
// 再对比证书的有效期
if sslItem.StartDate != certX509.NotBefore.Unix() || sslItem.ExpireDate != certX509.NotAfter.Unix() {
continue
}
// 最后对比证书内容
sslCenterGetResp, err := u.sdkClient.SslCenterGet(sslItem.ID)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.Get'")
}
var isSameCert bool
if sslCenterGetResp.Data != nil {
if sslCenterGetResp.Data.Cert == certPem {
isSameCert = true
} else {
oldCertX509, err := certutil.ParseCertificateFromPEM(sslCenterGetResp.Data.Cert)
if err != nil {
continue
}
isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
}
}
// 如果已存在相同证书,直接返回
if isSameCert {
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", sslItem.ID),
}, nil
}
}
}
if sslCenterListResp.Data == nil || len(sslCenterListResp.Data.Records) < int(sslCenterListPerPage) {
break
} else {
sslCenterListPage++
}
}
return nil, nil
}
func createSdkClient(apiKey string) (*rainyunsdk.Client, error) {
if apiKey == "" {
return nil, errors.New("invalid rainyun api key")
}
client := rainyunsdk.NewClient(apiKey)
return client, nil
}

View File

@ -0,0 +1,67 @@
package rainyunsslcenter_test
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/rainyun-sslcenter"
)
var (
fInputCertPath string
fInputKeyPath string
fApiKey string
)
func init() {
argsPrefix := "CERTIMATE_UPLOADER_RAINYUNSSLCENTER_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./rainyun_sslcenter_test.go -args \
--CERTIMATE_UPLOADER_RAINYUNSSLCENTER_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_RAINYUNSSLCENTER_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_UPLOADER_RAINYUNSSLCENTER_APIKEY="your-api-key"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APIKEY: %v", fApiKey),
}, "\n"))
uploader, err := provider.NewUploader(&provider.UploaderConfig{
ApiKey: fApiKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
sres, _ := json.Marshal(res)
t.Logf("ok: %s", string(sres))
})
}

View File

@ -89,10 +89,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
u.logger.Debug("sdk request 'ussl.UploadNormalCertificate'", slog.Any("request", uploadNormalCertificateReq), slog.Any("response", uploadNormalCertificateResp))
if err != nil {
if uploadNormalCertificateResp != nil && uploadNormalCertificateResp.GetRetCode() == 80035 {
if res, err := u.getExistCert(ctx, certPem); err != nil {
if res, err := u.getCertIfExists(ctx, certPem); err != nil {
return nil, err
} else if res == nil {
return nil, errors.New("no certificate found")
return nil, errors.New("ucloud ssl: no certificate found")
} else {
u.logger.Info("ssl certificate already exists")
return res, nil
@ -112,7 +112,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
}, nil
}
func (u *UploaderProvider) getExistCert(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) {
func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPem)
if err != nil {

View File

@ -74,6 +74,18 @@ func GetOrDefaultInt32(dict map[string]any, key string, defaultValue int32) int3
}
}
if result, ok := value.(int64); ok {
if result != 0 {
return int32(result)
}
}
if result, ok := value.(int); ok {
if result != 0 {
return int32(result)
}
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 32); err == nil {
@ -126,6 +138,12 @@ func GetOrDefaultInt64(dict map[string]any, key string, defaultValue int64) int6
}
}
if result, ok := value.(int); ok {
if result != 0 {
return int64(result)
}
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 64); err == nil {

View File

@ -79,7 +79,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil {
return resp, fmt.Errorf("1panel api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("1panel api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
return resp, fmt.Errorf("1panel api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil

View File

@ -75,7 +75,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil {
return resp, fmt.Errorf("baishan api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("baishan api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
return resp, fmt.Errorf("baishan api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil

View File

@ -86,7 +86,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response,
if err != nil {
return resp, fmt.Errorf("baota api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("baota api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
return resp, fmt.Errorf("baota api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil

View File

@ -59,7 +59,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil {
return resp, fmt.Errorf("cachefly api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("cachefly api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
return resp, fmt.Errorf("cachefly api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil

View File

@ -3,17 +3,18 @@ package cdnflysdk
import (
"fmt"
"net/http"
"net/url"
)
func (c *Client) GetSite(req *GetSiteRequest) (*GetSiteResponse, error) {
resp := &GetSiteResponse{}
err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/v1/sites/%s", req.Id), req, resp)
err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/v1/sites/%s", url.PathEscape(req.Id)), req, resp)
return resp, err
}
func (c *Client) UpdateSite(req *UpdateSiteRequest) (*UpdateSiteResponse, error) {
resp := &UpdateSiteResponse{}
err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/sites/%s", req.Id), req, resp)
err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/sites/%s", url.PathEscape(req.Id)), req, resp)
return resp, err
}
@ -25,6 +26,6 @@ func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertif
func (c *Client) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
resp := &UpdateCertificateResponse{}
err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/certs/%s", req.Id), req, resp)
err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/certs/%s", url.PathEscape(req.Id)), req, resp)
return resp, err
}

View File

@ -65,7 +65,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil {
return resp, fmt.Errorf("cdnfly api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("cdnfly api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
return resp, fmt.Errorf("cdnfly api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil

View File

@ -60,7 +60,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil {
return resp, fmt.Errorf("dnsla api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("dnsla api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
return resp, fmt.Errorf("dnsla api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil

View File

@ -6,9 +6,8 @@ import (
"fmt"
"time"
"github.com/Edgio/edgio-api/applications/v7/dtos"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos"
)
// AccessTokenResponse represents the response from the token endpoint.

View File

@ -3,7 +3,7 @@ package edgio_api
import (
"context"
"github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos"
"github.com/Edgio/edgio-api/applications/v7/dtos"
)
type EdgioClientInterface interface {

View File

@ -0,0 +1,3 @@
module github.com/Edgio/edgio-api
go 1.23.0

View File

@ -82,7 +82,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response,
if err != nil {
return resp, fmt.Errorf("gname api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("gname api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
return resp, fmt.Errorf("gname api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil

30
internal/pkg/vendors/rainyun-sdk/api.go vendored Normal file
View File

@ -0,0 +1,30 @@
package rainyunsdk
import (
"fmt"
"net/http"
)
func (c *Client) SslCenterList(req *SslCenterListRequest) (*SslCenterListResponse, error) {
resp := &SslCenterListResponse{}
err := c.sendRequestWithResult(http.MethodGet, "/product/sslcenter", req, resp)
return resp, err
}
func (c *Client) SslCenterGet(id int32) (*SslCenterGetResponse, error) {
resp := &SslCenterGetResponse{}
err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/product/sslcenter/%d", id), nil, resp)
return resp, err
}
func (c *Client) SslCenterCreate(req *SslCenterCreateRequest) (*SslCenterCreateResponse, error) {
resp := &SslCenterCreateResponse{}
err := c.sendRequestWithResult(http.MethodPost, "/product/sslcenter/", req, resp)
return resp, err
}
func (c *Client) RcdnInstanceSslBind(id int32, req *RcdnInstanceSslBindRequest) (*RcdnInstanceSslBindResponse, error) {
resp := &RcdnInstanceSslBindResponse{}
err := c.sendRequestWithResult(http.MethodPost, fmt.Sprintf("/product/rcdn/instance/%d/ssl_bind", id), req, resp)
return resp, err
}

View File

@ -0,0 +1,74 @@
package rainyunsdk
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
type Client struct {
apiKey string
client *resty.Client
}
func NewClient(apiKey string) *Client {
client := resty.New()
return &Client{
apiKey: apiKey,
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().SetHeader("x-api-key", c.apiKey)
req.Method = method
req.URL = "https://api.v2.rainyun.com" + path
if strings.EqualFold(method, http.MethodGet) {
if params != nil {
jsonb, _ := json.Marshal(params)
req = req.SetQueryParam("options", string(jsonb))
}
} else {
req = req.
SetHeader("Content-Type", "application/json").
SetBody(params)
}
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("rainyun api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("rainyun 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("rainyun api error: failed to parse response: %w", err)
} else if errcode := result.GetCode(); errcode/100 != 2 {
return fmt.Errorf("rainyun api error: %d - %s", errcode, result.GetMessage())
}
return nil
}

View File

@ -0,0 +1,83 @@
package rainyunsdk
type BaseResponse interface {
GetCode() int32
GetMessage() string
}
type baseResponse struct {
Code *int32 `json:"code,omitempty"`
Message *string `json:"message,omitempty"`
}
func (r *baseResponse) GetCode() int32 {
if r.Code != nil {
return *r.Code
}
return 0
}
func (r *baseResponse) GetMessage() string {
if r.Message != nil {
return *r.Message
}
return ""
}
type SslCenterListFilters struct {
Domain *string `json:"Domain,omitempty"`
}
type SslCenterListRequest struct {
Filters *SslCenterListFilters `json:"columnFilters,omitempty"`
Sort []*string `json:"sort,omitempty"`
Page *int32 `json:"page,omitempty"`
PerPage *int32 `json:"perPage,omitempty"`
}
type SslCenterListResponse struct {
baseResponse
Data *struct {
TotalRecords int32 `json:"TotalRecords"`
Records []*struct {
ID int32 `json:"ID"`
UID int32 `json:"UID"`
Domain string `json:"Domain"`
Issuer string `json:"Issuer"`
StartDate int64 `json:"StartDate"`
ExpireDate int64 `json:"ExpDate"`
UploadTime int64 `json:"UploadTime"`
} `json:"Records"`
} `json:"data,omitempty"`
}
type SslCenterGetResponse struct {
baseResponse
Data *struct {
Cert string `json:"Cert"`
Key string `json:"Key"`
Domain string `json:"DomainName"`
Issuer string `json:"Issuer"`
StartDate int64 `json:"StartDate"`
ExpireDate int64 `json:"ExpDate"`
RemainDays int32 `json:"RemainDays"`
} `json:"data,omitempty"`
}
type SslCenterCreateRequest struct {
Cert string `json:"cert"`
Key string `json:"key"`
}
type SslCenterCreateResponse struct {
baseResponse
}
type RcdnInstanceSslBindRequest struct {
CertId int32 `json:"cert_id"`
Domains []string `json:"domains"`
}
type RcdnInstanceSslBindResponse struct {
baseResponse
}

View File

@ -47,7 +47,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response,
if err != nil {
return resp, fmt.Errorf("safeline api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("safeline api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
return resp, fmt.Errorf("safeline api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil

View File

@ -64,7 +64,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil {
return resp, fmt.Errorf("upyun api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("upyun api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body())
return resp, fmt.Errorf("upyun api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil

View File

@ -0,0 +1,58 @@
package cdn
import (
"fmt"
"net/http"
"net/url"
"github.com/go-resty/resty/v2"
)
func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {
resp := &CreateCertificateResponse{}
r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/certificates", req, resp, func(r *resty.Request) {
r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp))
})
if err != nil {
return resp, err
}
resp.CertificateUrl = r.Header().Get("Location")
return resp, err
}
func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
resp := &UpdateCertificateResponse{}
r, err := c.client.SendRequestWithResult(http.MethodPatch, fmt.Sprintf("/cdn/certificates/%s", url.PathEscape(certificateId)), req, resp, func(r *resty.Request) {
r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp))
})
if err != nil {
return resp, err
}
resp.CertificateUrl = r.Header().Get("Location")
return resp, err
}
func (c *Client) GetHostnameDetail(hostname string) (*GetHostnameDetailResponse, error) {
resp := &GetHostnameDetailResponse{}
_, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/hostnames/%s", url.PathEscape(hostname)), nil, resp)
return resp, err
}
func (c *Client) CreateDeploymentTask(req *CreateDeploymentTaskRequest) (*CreateDeploymentTaskResponse, error) {
resp := &CreateDeploymentTaskResponse{}
r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/deploymentTasks", req, resp)
if err != nil {
return resp, err
}
resp.DeploymentTaskUrl = r.Header().Get("Location")
return resp, err
}
func (c *Client) GetDeploymentTaskDetail(deploymentTaskId string) (*GetDeploymentTaskDetailResponse, error) {
resp := &GetDeploymentTaskDetailResponse{}
_, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/deploymentTasks/%s", deploymentTaskId), nil, resp)
return resp, err
}

View File

@ -0,0 +1,20 @@
package cdn
import (
"time"
"github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi"
)
type Client struct {
client *openapi.Client
}
func NewClient(accessKey, secretKey string) *Client {
return &Client{client: openapi.NewClient(accessKey, secretKey)}
}
func (c *Client) WithTimeout(timeout time.Duration) *Client {
c.client.WithTimeout(timeout)
return c
}

View File

@ -0,0 +1,107 @@
package cdn
import (
"github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi"
)
type baseResponse struct {
RequestId *string `json:"-"`
Code *string `json:"code,omitempty"`
Message *string `json:"message,omitempty"`
}
var _ openapi.Result = (*baseResponse)(nil)
func (r *baseResponse) SetRequestId(requestId string) {
r.RequestId = &requestId
}
type CertificateVersion struct {
Comments *string `json:"comments,omitempty"`
PrivateKey *string `json:"privateKey,omitempty"`
Certificate *string `json:"certificate,omitempty"`
ChainCert *string `json:"chainCert,omitempty"`
IdentificationInfo *CertificateVersionIdentificationInfo `json:"identificationInfo,omitempty"`
}
type CertificateVersionIdentificationInfo struct {
Country *string `json:"country,omitempty"`
State *string `json:"state,omitempty"`
City *string `json:"city,omitempty"`
Company *string `json:"company,omitempty"`
Department *string `json:"department,omitempty"`
CommonName *string `json:"commonName,omitempty" required:"true"`
Email *string `json:"email,omitempty"`
SubjectAlternativeNames *[]string `json:"subjectAlternativeNames,omitempty" required:"true"`
}
type CreateCertificateRequest struct {
Timestamp int64 `json:"-"`
Name *string `json:"name,omitempty" required:"true"`
Description *string `json:"description,omitempty"`
AutoRenew *string `json:"autoRenew,omitempty"`
ForceRenew *bool `json:"forceRenew,omitempty"`
NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"`
}
type CreateCertificateResponse struct {
baseResponse
CertificateUrl string `json:"-"`
}
type UpdateCertificateRequest struct {
Timestamp int64 `json:"-"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
AutoRenew *string `json:"autoRenew,omitempty"`
ForceRenew *bool `json:"forceRenew,omitempty"`
NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"`
}
type UpdateCertificateResponse struct {
baseResponse
CertificateUrl string `json:"-"`
}
type HostnameProperty struct {
PropertyId string `json:"propertyId"`
Version int32 `json:"version"`
CertificateId *string `json:"certificateId,omitempty"`
}
type GetHostnameDetailResponse struct {
baseResponse
Hostname string `json:"hostname"`
PropertyInProduction *HostnameProperty `json:"propertyInProduction,omitempty"`
PropertyInStaging *HostnameProperty `json:"propertyInStaging,omitempty"`
}
type DeploymentTaskAction struct {
Action *string `json:"action,omitempty" required:"true"`
PropertyId *string `json:"propertyId,omitempty"`
CertificateId *string `json:"certificateId,omitempty"`
Version *string `json:"version,omitempty"`
}
type CreateDeploymentTaskRequest struct {
Name *string `json:"name,omitempty"`
Target *string `json:"target,omitempty" required:"true"`
Actions *[]DeploymentTaskAction `json:"actions,omitempty" required:"true"`
Webhook *string `json:"webhook,omitempty"`
}
type CreateDeploymentTaskResponse struct {
baseResponse
DeploymentTaskUrl string `json:"-"`
}
type GetDeploymentTaskDetailResponse struct {
baseResponse
Target string `json:"target"`
Actions []DeploymentTaskAction `json:"actions"`
Status string `json:"status"`
StatusDetails string `json:"statusDetails"`
SubmissionTime string `json:"submissionTime"`
FinishTime string `json:"finishTime"`
ApiRequestId string `json:"apiRequestId"`
}

View File

@ -0,0 +1,187 @@
package openapi
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
type Client struct {
accessKey string
secretKey string
client *resty.Client
}
type Result interface {
SetRequestId(requestId string)
}
func NewClient(accessKey, secretKey string) *Client {
client := resty.New().
SetBaseURL("https://open.chinanetcenter.com").
SetHeader("Host", "open.chinanetcenter.com").
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
// Step 1: Get request method
method := req.Method
method = strings.ToUpper(method)
// Step 2: Get request path
path := "/"
if req.URL != nil {
path = req.URL.Path
}
// Step 3: Get unencoded query string
queryString := ""
if method != http.MethodPost && req.URL != nil {
queryString = req.URL.RawQuery
s, err := url.QueryUnescape(queryString)
if err != nil {
return err
}
queryString = s
}
// Step 4: Get canonical headers & signed headers
canonicalHeaders := "" +
"content-type:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Content-Type"))) + "\n" +
"host:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Host"))) + "\n"
signedHeaders := "content-type;host"
// Step 5: Get request payload
payload := ""
if method != http.MethodGet && req.Body != nil {
reader, err := req.GetBody()
if err != nil {
return err
}
defer reader.Close()
payloadb, err := io.ReadAll(reader)
if err != nil {
return err
}
payload = string(payloadb)
}
hashedPayload := sha256.Sum256([]byte(payload))
hashedPayloadHex := strings.ToLower(hex.EncodeToString(hashedPayload[:]))
// Step 6: Get timestamp
var reqtime time.Time
timestampString := req.Header.Get("x-cnc-timestamp")
if timestampString == "" {
reqtime = time.Now().UTC()
timestampString = fmt.Sprintf("%d", reqtime.Unix())
} else {
timestamp, err := strconv.ParseInt(timestampString, 10, 64)
if err != nil {
return err
}
reqtime = time.Unix(timestamp, 0).UTC()
}
// Step 7: Get canonical request string
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, queryString, canonicalHeaders, signedHeaders, hashedPayloadHex)
hashedCanonicalRequest := sha256.Sum256([]byte(canonicalRequest))
hashedCanonicalRequestHex := strings.ToLower(hex.EncodeToString(hashedCanonicalRequest[:]))
// Step 8: String to sign
const SignAlgorithmHeader = "CNC-HMAC-SHA256"
stringToSign := fmt.Sprintf("%s\n%s\n%s", SignAlgorithmHeader, timestampString, hashedCanonicalRequestHex)
hmac := hmac.New(sha256.New, []byte(secretKey))
hmac.Write([]byte(stringToSign))
sign := hmac.Sum(nil)
signHex := strings.ToLower(hex.EncodeToString(sign))
// Step 9: Add headers to request
req.Header.Set("x-cnc-accessKey", accessKey)
req.Header.Set("x-cnc-timestamp", timestampString)
req.Header.Set("x-cnc-auth-method", "AKSK")
req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", SignAlgorithmHeader, accessKey, signedHeaders, signHex))
req.Header.Set("Date", reqtime.Format("Mon, 02 Jan 2006 15:04:05 GMT"))
return nil
})
return &Client{
accessKey: accessKey,
secretKey: secretKey,
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{}, configureReq ...func(req *resty.Request)) (*resty.Response, error) {
req := c.client.R()
req.Method = method
req.URL = 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)
} else {
req = req.SetBody(params)
}
for _, fn := range configureReq {
fn(req)
}
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("wangsu api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("wangsu 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 Result, configureReq ...func(req *resty.Request)) (*resty.Response, error) {
resp, err := c.sendRequest(method, path, params, configureReq...)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &result)
result.SetRequestId(resp.Header().Get("x-cnc-request-id"))
}
return resp, err
}
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return resp, fmt.Errorf("wangsu api error: failed to parse response: %w", err)
}
result.SetRequestId(resp.Header().Get("x-cnc-request-id"))
return resp, nil
}

View File

@ -262,8 +262,8 @@ func init() {
Type string `json:"type"`
Name string `json:"name"`
Config map[string]any `json:"config"`
Inputs map[string]any `json:"inputs"`
Outputs map[string]any `json:"outputs"`
Inputs []map[string]any `json:"inputs"`
Outputs []map[string]any `json:"outputs"`
Next *dWorkflowNode `json:"next,omitempty"`
Branches []dWorkflowNode `json:"branches,omitempty"`
Validated bool `json:"validated"`

View File

@ -0,0 +1,91 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
"hidden": false,
"id": "hwy7m03o",
"maxSelect": 1,
"name": "provider",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"1panel",
"acmehttpreq",
"akamai",
"aliyun",
"aws",
"azure",
"baiducloud",
"baishan",
"baotapanel",
"byteplus",
"buypass",
"cachefly",
"cdnfly",
"cloudflare",
"cloudns",
"cmcccloud",
"ctcccloud",
"cucccloud",
"desec",
"dnsla",
"dogecloud",
"dynv6",
"edgio",
"fastly",
"gname",
"gcore",
"godaddy",
"goedge",
"googletrustservices",
"huaweicloud",
"jdcloud",
"k8s",
"letsencrypt",
"letsencryptstaging",
"local",
"namecheap",
"namedotcom",
"namesilo",
"ns1",
"porkbun",
"powerdns",
"qiniu",
"qingcloud",
"rainyun",
"safeline",
"ssh",
"sslcom",
"tencentcloud",
"ucloud",
"upyun",
"vercel",
"volcengine",
"wangsu",
"webhook",
"westcn",
"zerossl"
]
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
return nil
})
}

View File

@ -0,0 +1 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" x="0" y="0" width="200" height="200" viewBox="0 0 340 100"><g><path d="M290,61.5c-4.1,4.6-29.3,36.7-56.9,26.7c-3.6-1.3-7.2-3.3-10.9-6.2c3.8,0.2,7.5,0,10.9-0.3 c5.3-0.5,9.8-1.5,12.4-2.9c6.1-3,4.5-13.2-12.4-16.2c-0.4-0.1-0.9-0.2-1.3-0.2c-11.3-1.7-19.5,1.7-23.7,5.3 c-0.1,0-0.3-0.1-0.4-0.2c-2.6-1.1-5.2-2.2-7.8-3.2c20-0.9,23.4-14.1,4.8-26.4c-7.5-5-16.3-9.4-25.6-13.4 c-10.7-4.5-22.3-8.5-33.7-11.7c-7.9-2.2-15.7-4.1-23.1-5.7C109.9,4.6,98.7,2.8,90,2C71.1,0.3,58.1,1.5,50.8,5.8 C47.7,7.6,46,10,46,13v0.3c0,1.6,0.5,3.3,1.4,5.2c3,6.3,11.1,13.5,24.1,21.5c11.5,7.1,28.8,15.2,43.5,20.3 c0.1,0.8,0.2,1.6,0.6,2.6c0.9,2.4,3.1,5.2,6.7,8.3c0.5,0.5,1.1,1,1.8,1.5c5.7,4.5,12.7,8.7,21.1,12.2l0.2,0.1 c10.9,4.6,22.6,8.2,33.7,10.8c16.7,3.8,32.2,5.4,42.6,4.9c0.5-0.2,0.7-0.4,0.4-0.6c-0.1-0.1-0.2-0.2-0.4-0.2 c-5.7-0.4-12.9-1.2-21.7-2.3c-7.3-1-14.3-2.3-21-3.8c-12.1-2.7-23.1-6.2-33-10.5c-0.2-0.1-0.4-0.2-0.7-0.3 c-6.7-2.9-10.5-5.7-11.4-8.2c-1.2-3.4,1.9-5.2,5-6c2,0.7,4.1,1.3,6.4,1.9c10.2,2.9,22.2,5.5,33.7,7.4c11.1,1.9,21.7,3.2,29.5,3.6 c7.6,6.7,16.1,10.2,24.6,11.3c15.2,1.8,30.4-4.1,41.1-13.8c7.3-6.6,13.2-13,16.3-17L290,61.5z M145.3,30.3 c8.5,0.3,17.4,1.3,26.7,3.1c2.5,0.5,4.8,1,7,1.5c6.7,1.6,11.5,3.3,14.5,5.2c3.4,2.1,4.1,3.7,2.1,4.9c-1.5,0.9-4.6,1.5-9.4,2 c-2.4,0.2-4.8,0.4-7.2,0.4c-2.4,0.1-4.9,0-7.4,0c-4.9-0.2-9.8-0.9-14.8-1.8c-4.9-1-8.3-2-10-3c-1.3-0.8-1.6-1.4-1-1.9 c0.4-0.2,1.1-0.4,2.3-0.5c0.9-0.1,1.5-0.4,2.1-0.7c1.1-0.7,1.4-1.6,0.8-2.9c-0.7-1.3-2-2.6-4-3.8c-0.5-0.3-1.1-0.6-1.7-1 c-0.7-0.4-1.6-0.8-2.6-1.3C143.6,30.3,144.4,30.3,145.3,30.3z M145.3,51c-6-0.5-11.4-0.5-16.2,0.1c-2.7,0.3-5,0.7-6.9,1.3 c-1.3,0.4-2.5,0.9-3.4,1.4c-0.3-0.1-0.6-0.2-1-0.4c-12.8-4.5-26.4-14.1-19.6-18.1c4.8-2.8,12.8-4.4,24-4.8c2-0.1,4.1-0.1,6.3-0.1 c-1.7,5.1,2.7,10.8,12.9,17.1c1.3,0.8,2.6,1.6,3.8,2.3c0.9,0.5,1.7,1,2.6,1.4C147,51.1,146.1,51.1,145.3,51z M206.7,79.4 c-8.5-0.8-18.3-2.9-27.7-5.2c-10.6-2.7-20.7-5.7-27.7-7.9c6.4-1.5,14.8-2.2,25.6-2c0.7,0,1.4,0,2.1,0c10.8,0.4,20,1.9,27.8,4.6 C204.6,71.5,203.8,74.9,206.7,79.4z M233.1,66.9c0.4,0,0.7,0,1.1,0.1c3.6,0.4,6.7,0.9,9.8,3.1c3.5,2.4,3.5,4.4,2.1,6.5 c-1.3,2.1-7.1,2.9-13,3.2c-4.6,0.2-9.3,0.1-12.2,0C216.1,74.7,221.9,66.4,233.1,66.9z" fill="#005BAC"/></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -51,6 +51,7 @@ import AccessFormUCloudConfig from "./AccessFormUCloudConfig";
import AccessFormUpyunConfig from "./AccessFormUpyunConfig";
import AccessFormVercelConfig from "./AccessFormVercelConfig";
import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig";
import AccessFormWangsuConfig from "./AccessFormWangsuConfig";
import AccessFormWebhookConfig from "./AccessFormWebhookConfig";
import AccessFormWestcnConfig from "./AccessFormWestcnConfig";
import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig";
@ -229,6 +230,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormVercelConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.VOLCENGINE:
return <AccessFormVolcEngineConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.WANGSU:
return <AccessFormWangsuConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.WEBHOOK:
return <AccessFormWebhookConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.WESTCN:

View File

@ -0,0 +1,76 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForWangsu } from "@/domain/access";
type AccessFormWangsuConfigFieldValues = Nullish<AccessConfigForWangsu>;
export type AccessFormWangsuConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormWangsuConfigFieldValues;
onValuesChange?: (values: AccessFormWangsuConfigFieldValues) => void;
};
const initFormModel = (): AccessFormWangsuConfigFieldValues => {
return {
accessKeyId: "",
accessKeySecret: "",
};
};
const AccessFormWangsuConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange: onValuesChange }: AccessFormWangsuConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
accessKeyId: z
.string()
.min(1, t("access.form.wangsu_access_key_id.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(),
accessKeySecret: z
.string()
.min(1, t("access.form.wangsu_access_key_secret.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="accessKeyId"
label={t("access.form.wangsu_access_key_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.wangsu_access_key_id.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.wangsu_access_key_id.placeholder")} />
</Form.Item>
<Form.Item
name="accessKeySecret"
label={t("access.form.wangsu_access_key_secret.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.wangsu_access_key_secret.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.wangsu_access_key_secret.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormWangsuConfig;

View File

@ -9,6 +9,8 @@ import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalk
import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields";
import NotifyChannelEditFormGotifyFields from "./NotifyChannelEditFormGotifyFields.tsx";
import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields";
import NotifyChannelEditFormMattermostFields from "./NotifyChannelEditFormMattermostFields.tsx";
import NotifyChannelEditFormPushoverFields from "./NotifyChannelEditFormPushoverFields";
import NotifyChannelEditFormPushPlusFields from "./NotifyChannelEditFormPushPlusFields";
import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields";
import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields";
@ -54,6 +56,10 @@ const NotifyChannelEditForm = forwardRef<NotifyChannelEditFormInstance, NotifyCh
return <NotifyChannelEditFormGotifyFields />;
case NOTIFY_CHANNELS.LARK:
return <NotifyChannelEditFormLarkFields />;
case NOTIFY_CHANNELS.MATTERMOST:
return <NotifyChannelEditFormMattermostFields />;
case NOTIFY_CHANNELS.PUSHOVER:
return <NotifyChannelEditFormPushoverFields />;
case NOTIFY_CHANNELS.PUSHPLUS:
return <NotifyChannelEditFormPushPlusFields />;
case NOTIFY_CHANNELS.SERVERCHAN:

View File

@ -0,0 +1,64 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormMattermostFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
serverUrl: z
.string({ message: t("settings.notification.channel.form.mattermost_server_url.placeholder") })
.url(t("common.errmsg.url_invalid")),
channelId: z
.string({ message: t("settings.notification.channel.form.mattermost_channel_id.placeholder") })
.nonempty(t("settings.notification.channel.form.mattermost_channel_id.placeholder")),
username: z
.string({ message: t("settings.notification.channel.form.mattermost_username.placeholder") })
.nonempty(t("settings.notification.channel.form.mattermost_username.placeholder")),
password: z
.string({ message: t("settings.notification.channel.form.mattermost_password.placeholder") })
.nonempty(t("settings.notification.channel.form.mattermost_password.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="serverUrl"
label={t("settings.notification.channel.form.mattermost_server_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.mattermost_server_url.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.mattermost_server_url.placeholder")} />
</Form.Item>
<Form.Item
name="channelId"
label={t("settings.notification.channel.form.mattermost_channel_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.mattermost_channel_id.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.mattermost_channel_id.placeholder")} />
</Form.Item>
<Form.Item
name="username"
label={t("settings.notification.channel.form.mattermost_username.label")}
rules={[formRule]}
>
<Input placeholder={t("settings.notification.channel.form.mattermost_username.placeholder")} />
</Form.Item>
<Form.Item
name="password"
label={t("settings.notification.channel.form.mattermost_password.label")}
rules={[formRule]}
>
<Input.Password placeholder={t("settings.notification.channel.form.mattermost_password.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormMattermostFields;

View File

@ -0,0 +1,41 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormPushoverFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
token: z
.string({ message: t("settings.notification.channel.form.pushover_token.placeholder") })
.nonempty(t("settings.notification.channel.form.pushover_token.placeholder")),
user: z
.string({ message: t("settings.notification.channel.form.pushover_user.placeholder") })
.nonempty(t("settings.notification.channel.form.pushover_user.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="token"
label={t("settings.notification.channel.form.pushover_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.pushover_token.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.pushover_token.placeholder")} />
</Form.Item>
<Form.Item
name="user"
label={t("settings.notification.channel.form.pushover_user.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.pushover_user.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.pushover_user.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormPushoverFields;

View File

@ -50,12 +50,14 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid
<div className="flex max-w-full items-center justify-between gap-4 overflow-hidden">
<Space className="max-w-full grow truncate" size={4}>
<Avatar src={provider.icon} size="small" />
<Typography.Text className="leading-loose" type={provider.builtin ? "secondary" : undefined} delete={provider.builtin ? true : undefined} ellipsis>
<Typography.Text className="leading-loose" type={provider.builtin ? "secondary" : undefined} ellipsis>
{t(provider.name)}
</Typography.Text>
</Space>
{showOptionTags && (
<div>
<Show when={provider.builtin}>
<Tag color="grey">{t("access.props.provider.builtin")}</Tag>
</Show>
<Show when={showOptionTagForDNS && provider.usages.includes(ACCESS_USAGES.DNS)}>
<Tag color="peru">{t("access.props.provider.usage.dns")}</Tag>
</Show>
@ -69,7 +71,6 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid
<Tag color="mediumaquamarine">{t("access.props.provider.usage.notification")}</Tag>
</Show>
</div>
)}
</div>
);
};

View File

@ -11,7 +11,7 @@ import {
SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons";
import { useRequest } from "ahooks";
import { Button, Empty, Modal, Space, Table, type TableProps, Tag, Tooltip, notification } from "antd";
import { Alert, Button, Empty, Modal, Space, Table, type TableProps, Tag, Tooltip, notification } from "antd";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
@ -284,6 +284,8 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
{NotificationContextHolder}
<div className={className} style={style}>
<Alert className="mb-4" type="warning" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_run.table.alert") }}></span>} />
<Table<WorkflowRunModel>
columns={tableColumns}
dataSource={tableData}

View File

@ -56,6 +56,7 @@ import DeployNodeConfigFormLocalConfig from "./DeployNodeConfigFormLocalConfig";
import DeployNodeConfigFormQiniuCDNConfig from "./DeployNodeConfigFormQiniuCDNConfig";
import DeployNodeConfigFormQiniuKodoConfig from "./DeployNodeConfigFormQiniuKodoConfig";
import DeployNodeConfigFormQiniuPiliConfig from "./DeployNodeConfigFormQiniuPiliConfig";
import DeployNodeConfigFormRainYunRCDNConfig from "./DeployNodeConfigFormRainYunRCDNConfig";
import DeployNodeConfigFormSafeLineConfig from "./DeployNodeConfigFormSafeLineConfig";
import DeployNodeConfigFormSSHConfig from "./DeployNodeConfigFormSSHConfig.tsx";
import DeployNodeConfigFormTencentCloudCDNConfig from "./DeployNodeConfigFormTencentCloudCDNConfig.tsx";
@ -80,6 +81,7 @@ import DeployNodeConfigFormVolcEngineDCDNConfig from "./DeployNodeConfigFormVolc
import DeployNodeConfigFormVolcEngineImageXConfig from "./DeployNodeConfigFormVolcEngineImageXConfig.tsx";
import DeployNodeConfigFormVolcEngineLiveConfig from "./DeployNodeConfigFormVolcEngineLiveConfig.tsx";
import DeployNodeConfigFormVolcEngineTOSConfig from "./DeployNodeConfigFormVolcEngineTOSConfig.tsx";
import DeployNodeConfigFormWangsuCDNProConfig from "./DeployNodeConfigFormWangsuCDNProConfig.tsx";
import DeployNodeConfigFormWebhookConfig from "./DeployNodeConfigFormWebhookConfig.tsx";
type DeployNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForDeploy>;
@ -251,6 +253,8 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
return <DeployNodeConfigFormQiniuKodoConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.QINIU_PILI:
return <DeployNodeConfigFormQiniuPiliConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.RAINYUN_RCDN:
return <DeployNodeConfigFormRainYunRCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.SAFELINE:
return <DeployNodeConfigFormSafeLineConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.SSH:
@ -299,6 +303,8 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
return <DeployNodeConfigFormVolcEngineLiveConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_TOS:
return <DeployNodeConfigFormVolcEngineTOSConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.WANGSU_CDNPRO:
return <DeployNodeConfigFormWangsuCDNProConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.WEBHOOK:
return <DeployNodeConfigFormWebhookConfig {...nestedFormProps} />;
}

View File

@ -4,7 +4,7 @@ import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
type DeployNodeConfigFormGcoreCDNConfigFieldValues = Nullish<{
resourceId?: string | number;
resourceId: string | number;
}>;
export type DeployNodeConfigFormGcoreCDNConfigProps = {
@ -27,7 +27,7 @@ const DeployNodeConfigFormGcoreCDNConfig = ({ form: formInst, formName, disabled
const formSchema = z.object({
resourceId: z.union([z.string(), z.number()]).refine((v) => {
return /^\d+$/.test(v + "") && +v > 0;
}, t("workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder")),
}, t("workflow_node.deploy.form.gcore_cdn_resource_id.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);

View File

@ -0,0 +1,80 @@
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 DeployNodeConfigFormRainYunRCDNConfigFieldValues = Nullish<{
instanceId: string | number;
domain: string;
}>;
export type DeployNodeConfigFormRainYunRCDNConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: DeployNodeConfigFormRainYunRCDNConfigFieldValues;
onValuesChange?: (values: DeployNodeConfigFormRainYunRCDNConfigFieldValues) => void;
};
const initFormModel = (): DeployNodeConfigFormRainYunRCDNConfigFieldValues => {
return {
instanceId: "",
};
};
const DeployNodeConfigFormRainYunRCDNConfig = ({
form: formInst,
formName,
disabled,
initialValues,
onValuesChange,
}: DeployNodeConfigFormRainYunRCDNConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
instanceId: z.union([z.string(), z.number()]).refine((v) => {
return /^\d+$/.test(v + "") && +v > 0;
}, t("workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder")),
domain: z
.string({ message: t("workflow_node.deploy.form.rainyun_rcdn_domain.placeholder") })
.refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="instanceId"
label={t("workflow_node.deploy.form.rainyun_rcdn_instance_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.rainyun_rcdn_instance_id.tooltip") }}></span>}
>
<Input type="number" placeholder={t("workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder")} />
</Form.Item>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.rainyun_rcdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.rainyun_rcdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.rainyun_rcdn_domain.placeholder")} />
</Form.Item>
</Form>
);
};
export default DeployNodeConfigFormRainYunRCDNConfig;

View File

@ -113,7 +113,7 @@ const DeployNodeConfigFormVolcEngineALBConfig = ({
<Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>
<Form.Item
name="listenerId"
name="loadbalancerId"
label={t("workflow_node.deploy.form.volcengine_alb_loadbalancer_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.volcengine_alb_loadbalancer_id.tooltip") }}></span>}

View File

@ -104,7 +104,7 @@ const DeployNodeConfigFormVolcEngineCLBConfig = ({
<Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER}>
<Form.Item
name="listenerId"
name="loadbalancerId"
label={t("workflow_node.deploy.form.volcengine_clb_loadbalancer_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.volcengine_clb_loadbalancer_id.tooltip") }}></span>}

View File

@ -0,0 +1,107 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName } from "@/utils/validators";
type DeployNodeConfigFormBaishanCDNConfigFieldValues = Nullish<{
environment: string;
domain: string;
certificateId?: string;
webhookId?: string;
}>;
export type DeployNodeConfigFormBaishanCDNConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: DeployNodeConfigFormBaishanCDNConfigFieldValues;
onValuesChange?: (values: DeployNodeConfigFormBaishanCDNConfigFieldValues) => void;
};
const ENVIRONMENT_PRODUCTION = "production" as const;
const ENVIRONMENT_STAGING = "stating" as const;
const initFormModel = (): DeployNodeConfigFormBaishanCDNConfigFieldValues => {
return {
environment: ENVIRONMENT_PRODUCTION,
};
};
const DeployNodeConfigFormBaishanCDNConfig = ({
form: formInst,
formName,
disabled,
initialValues,
onValuesChange,
}: DeployNodeConfigFormBaishanCDNConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
resourceType: z.union([z.literal(ENVIRONMENT_PRODUCTION), z.literal(ENVIRONMENT_STAGING)], {
message: t("workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder"),
}),
domain: z
.string({ message: t("workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder") })
.refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")),
certificateId: z.string().nullish(),
webhookId: z.string().nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item name="environment" label={t("workflow_node.deploy.form.wangsu_cdnpro_environment.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder")}>
<Select.Option key={ENVIRONMENT_PRODUCTION} value={ENVIRONMENT_PRODUCTION}>
{t("workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label")}
</Select.Option>
<Select.Option key={ENVIRONMENT_STAGING} value={ENVIRONMENT_STAGING}>
{t("workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.wangsu_cdnpro_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.wangsu_cdnpro_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder")} />
</Form.Item>
<Form.Item
name="certificateId"
label={t("workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder")} />
</Form.Item>
<Form.Item
name="webhookId"
label={t("workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder")} />
</Form.Item>
</Form>
);
};
export default DeployNodeConfigFormBaishanCDNConfig;

View File

@ -47,6 +47,7 @@ export interface AccessModel extends BaseModel {
| AccessConfigForUpyun
| AccessConfigForVercel
| AccessConfigForVolcEngine
| AccessConfigForWangsu
| AccessConfigForWebhook
| AccessConfigForWestcn
| AccessConfigForZeroSSL
@ -268,6 +269,11 @@ export type AccessConfigForVolcEngine = {
secretAccessKey: string;
};
export type AccessConfigForWangsu = {
accessKeyId: string;
accessKeySecret: string;
};
export type AccessConfigForWebhook = {
url: string;
allowInsecureConnections?: boolean;

View File

@ -50,6 +50,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
UPYUN: "upyun",
VERCEL: "vercel",
VOLCENGINE: "volcengine",
WANGSU: "wangsu",
WEBHOOK: "webhook",
WESTCN: "westcn",
ZEROSSL: "zerossl",
@ -94,10 +95,12 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.AWS, "provider.aws", "/imgs/providers/aws.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.AZURE, "provider.azure", "/imgs/providers/azure.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.GCORE, "provider.gcore", "/imgs/providers/gcore.png", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.RAINYUN, "provider.rainyun", "/imgs/providers/rainyun.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.QINIU, "provider.qiniu", "/imgs/providers/qiniu.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.UPYUN, "provider.upyun", "/imgs/providers/upyun.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.BAISHAN, "provider.baishan", "/imgs/providers/baishan.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.WANGSU, "provider.wangsu", "/imgs/providers/wangsu.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.DOGECLOUD, "provider.dogecloud", "/imgs/providers/dogecloud.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.BYTEPLUS, "provider.byteplus", "/imgs/providers/byteplus.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.UCLOUD, "provider.ucloud", "/imgs/providers/ucloud.svg", [ACCESS_USAGES.HOSTING]],
@ -122,7 +125,6 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.PORKBUN, "provider.porkbun", "/imgs/providers/porkbun.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.VERCEL, "provider.vercel", "/imgs/providers/vercel.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.CMCCCLOUD, "provider.cmcccloud", "/imgs/providers/cmcccloud.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.RAINYUN, "provider.rainyun", "/imgs/providers/rainyun.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.WESTCN, "provider.westcn", "/imgs/providers/westcn.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.POWERDNS, "provider.powerdns", "/imgs/providers/powerdns.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.ACMEHTTPREQ, "provider.acmehttpreq", "/imgs/providers/acmehttpreq.svg", [ACCESS_USAGES.DNS]],
@ -344,6 +346,7 @@ export const DEPLOY_PROVIDERS = Object.freeze({
QINIU_CDN: `${ACCESS_PROVIDERS.QINIU}-cdn`,
QINIU_KODO: `${ACCESS_PROVIDERS.QINIU}-kodo`,
QINIU_PILI: `${ACCESS_PROVIDERS.QINIU}-pili`,
RAINYUN_RCDN: `${ACCESS_PROVIDERS.RAINYUN}-rcdn`,
SAFELINE: `${ACCESS_PROVIDERS.SAFELINE}`,
SSH: `${ACCESS_PROVIDERS.SSH}`,
TENCENTCLOUD_CDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cdn`,
@ -369,6 +372,7 @@ export const DEPLOY_PROVIDERS = Object.freeze({
VOLCENGINE_IMAGEX: `${ACCESS_PROVIDERS.VOLCENGINE}-imagex`,
VOLCENGINE_LIVE: `${ACCESS_PROVIDERS.VOLCENGINE}-live`,
VOLCENGINE_TOS: `${ACCESS_PROVIDERS.VOLCENGINE}-tos`,
WANGSU_CDNPRO: `${ACCESS_PROVIDERS.WANGSU}-cdnpro`,
WEBHOOK: `${ACCESS_PROVIDERS.WEBHOOK}`,
} as const);
@ -418,7 +422,7 @@ export const deployProvidersMap: Map<DeployProvider["type"] | string, DeployProv
[DEPLOY_PROVIDERS.ALIYUN_LIVE, "provider.aliyun.live", DEPLOY_CATEGORIES.AV],
[DEPLOY_PROVIDERS.ALIYUN_VOD, "provider.aliyun.vod", DEPLOY_CATEGORIES.AV],
[DEPLOY_PROVIDERS.ALIYUN_FC, "provider.aliyun.fc", DEPLOY_CATEGORIES.SERVERLESS],
[DEPLOY_PROVIDERS.ALIYUN_CAS, "provider.aliyun.cas", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.ALIYUN_CAS, "provider.aliyun.cas_upload", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.ALIYUN_CAS_DEPLOY, "provider.aliyun.cas_deploy", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.TENCENTCLOUD_COS, "provider.tencentcloud.cos", DEPLOY_CATEGORIES.STORAGE],
[DEPLOY_PROVIDERS.TENCENTCLOUD_CDN, "provider.tencentcloud.cdn", DEPLOY_CATEGORIES.CDN],
@ -429,16 +433,16 @@ export const deployProvidersMap: Map<DeployProvider["type"] | string, DeployProv
[DEPLOY_PROVIDERS.TENCENTCLOUD_CSS, "provider.tencentcloud.css", DEPLOY_CATEGORIES.AV],
[DEPLOY_PROVIDERS.TENCENTCLOUD_VOD, "provider.tencentcloud.vod", DEPLOY_CATEGORIES.AV],
[DEPLOY_PROVIDERS.TENCENTCLOUD_SCF, "provider.tencentcloud.scf", DEPLOY_CATEGORIES.SERVERLESS],
[DEPLOY_PROVIDERS.TENCENTCLOUD_SSL, "provider.tencentcloud.ssl", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.TENCENTCLOUD_SSL, "provider.tencentcloud.ssl_upload", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.TENCENTCLOUD_SSL_DEPLOY, "provider.tencentcloud.ssl_deploy", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.BAIDUCLOUD_CDN, "provider.baiducloud.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.BAIDUCLOUD_BLB, "provider.baiducloud.blb", DEPLOY_CATEGORIES.LOADBALANCE],
[DEPLOY_PROVIDERS.BAIDUCLOUD_APPBLB, "provider.baiducloud.appblb", DEPLOY_CATEGORIES.LOADBALANCE],
[DEPLOY_PROVIDERS.BAIDUCLOUD_CERT, "provider.baiducloud.cert", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.BAIDUCLOUD_CERT, "provider.baiducloud.cert_upload", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.HUAWEICLOUD_CDN, "provider.huaweicloud.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.HUAWEICLOUD_ELB, "provider.huaweicloud.elb", DEPLOY_CATEGORIES.LOADBALANCE],
[DEPLOY_PROVIDERS.HUAWEICLOUD_WAF, "provider.huaweicloud.waf", DEPLOY_CATEGORIES.FIREWALL],
[DEPLOY_PROVIDERS.HUAWEICLOUD_SCM, "provider.huaweicloud.scm", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.HUAWEICLOUD_SCM, "provider.huaweicloud.scm_upload", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.VOLCENGINE_TOS, "provider.volcengine.tos", DEPLOY_CATEGORIES.STORAGE],
[DEPLOY_PROVIDERS.VOLCENGINE_CDN, "provider.volcengine.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.VOLCENGINE_DCDN, "provider.volcengine.dcdn", DEPLOY_CATEGORIES.CDN],
@ -446,7 +450,7 @@ export const deployProvidersMap: Map<DeployProvider["type"] | string, DeployProv
[DEPLOY_PROVIDERS.VOLCENGINE_ALB, "provider.volcengine.alb", DEPLOY_CATEGORIES.LOADBALANCE],
[DEPLOY_PROVIDERS.VOLCENGINE_IMAGEX, "provider.volcengine.imagex", DEPLOY_CATEGORIES.STORAGE],
[DEPLOY_PROVIDERS.VOLCENGINE_LIVE, "provider.volcengine.live", DEPLOY_CATEGORIES.AV],
[DEPLOY_PROVIDERS.VOLCENGINE_CERTCENTER, "provider.volcengine.certcenter", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.VOLCENGINE_CERTCENTER, "provider.volcengine.certcenter_upload", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.JDCLOUD_ALB, "provider.jdcloud.alb", DEPLOY_CATEGORIES.LOADBALANCE],
[DEPLOY_PROVIDERS.JDCLOUD_CDN, "provider.jdcloud.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.JDCLOUD_LIVE, "provider.jdcloud.live", DEPLOY_CATEGORIES.AV],
@ -457,10 +461,12 @@ export const deployProvidersMap: Map<DeployProvider["type"] | string, DeployProv
[DEPLOY_PROVIDERS.UPYUN_FILE, "provider.upyun.file", DEPLOY_CATEGORIES.STORAGE],
[DEPLOY_PROVIDERS.UPYUN_CDN, "provider.upyun.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.BAISHAN_CDN, "provider.baishan.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.WANGSU_CDNPRO, "provider.wangsu.cdnpro", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.DOGECLOUD_CDN, "provider.dogecloud.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.BYTEPLUS_CDN, "provider.byteplus.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.UCLOUD_US3, "provider.ucloud.us3", DEPLOY_CATEGORIES.STORAGE],
[DEPLOY_PROVIDERS.UCLOUD_UCDN, "provider.ucloud.ucdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.RAINYUN_RCDN, "provider.rainyun.rcdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.AWS_CLOUDFRONT, "provider.aws.cloudfront", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.AWS_ACM, "provider.aws.acm", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.AZURE_KEYVAULT, "provider.azure.keyvault", DEPLOY_CATEGORIES.OTHER],

View File

@ -44,6 +44,8 @@ export const NOTIFY_CHANNELS = Object.freeze({
EMAIL: "email",
GOTIFY: "gotify",
LARK: "lark",
MATTERMOST: "mattermost",
PUSHOVER: "pushover",
PUSHPLUS: "pushplus",
SERVERCHAN: "serverchan",
TELEGRAM: "telegram",
@ -64,6 +66,8 @@ export type NotifyChannelsSettingsContent = {
[NOTIFY_CHANNELS.EMAIL]?: EmailNotifyChannelConfig;
[NOTIFY_CHANNELS.GOTIFY]?: GotifyNotifyChannelConfig;
[NOTIFY_CHANNELS.LARK]?: LarkNotifyChannelConfig;
[NOTIFY_CHANNELS.MATTERMOST]?: MattermostNotifyChannelConfig;
[NOTIFY_CHANNELS.PUSHOVER]?: PushoverNotifyChannelConfig;
[NOTIFY_CHANNELS.PUSHPLUS]?: PushPlusNotifyChannelConfig;
[NOTIFY_CHANNELS.SERVERCHAN]?: ServerChanNotifyChannelConfig;
[NOTIFY_CHANNELS.TELEGRAM]?: TelegramNotifyChannelConfig;
@ -106,6 +110,20 @@ export type LarkNotifyChannelConfig = {
enabled?: boolean;
};
export type MattermostNotifyChannelConfig = {
serverUrl: string;
channel: string;
username: string;
password: string;
enabled?: boolean;
}
export type PushoverNotifyChannelConfig = {
token: string;
user: string;
enabled?: boolean;
};
export type PushPlusNotifyChannelConfig = {
token: string;
enabled?: boolean;
@ -143,6 +161,8 @@ export const notifyChannelsMap: Map<NotifyChannel["type"], NotifyChannel> = new
[NOTIFY_CHANNELS.DINGTALK, "common.notifier.dingtalk"],
[NOTIFY_CHANNELS.GOTIFY, "common.notifier.gotify"],
[NOTIFY_CHANNELS.LARK, "common.notifier.lark"],
[NOTIFY_CHANNELS.MATTERMOST, "common.notifier.mattermost"],
[NOTIFY_CHANNELS.PUSHOVER, "common.notifier.pushover"],
[NOTIFY_CHANNELS.PUSHPLUS, "common.notifier.pushplus"],
[NOTIFY_CHANNELS.WECOM, "common.notifier.wecom"],
[NOTIFY_CHANNELS.TELEGRAM, "common.notifier.telegram"],

View File

@ -17,6 +17,7 @@
"access.props.provider.usage.hosting": "Hosting provider",
"access.props.provider.usage.ca": "Certificate authority",
"access.props.provider.usage.notification": "Notification channel",
"access.props.provider.builtin": "Built-in",
"access.props.range.both_dns_hosting": "Provider",
"access.props.range.ca_only": "Certificate authority",
"access.props.range.notify_only": "Notification channel",
@ -237,7 +238,7 @@
"access.form.qiniu_secret_key.tooltip": "For more information, see <a href=\"https://portal.qiniu.com/\" target=\"_blank\">https://portal.qiniu.com/</a>",
"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 <a href=\"https://www.rainyun.com/docs/account/racc/setting#api%E5%AF%86%E9%92%A5\" target=\"_blank\">https://www.rainyun.com/docs/account/racc/setting</a>",
"access.form.rainyun_api_key.tooltip": "For more information, see <a href=\"https://app.rainyun.com/account/settings/api-key\" target=\"_blank\">https://app.rainyun.com/account/settings/api-key</a>",
"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 <a href=\"https://docs.waf.chaitin.com/en/tutorials/install#use-web-ui\" target=\"_blank\">https://docs.waf.chaitin.com/en/tutorials/install</a>",
@ -297,6 +298,12 @@
"access.form.volcengine_secret_access_key.label": "VolcEngine SecretAccessKey",
"access.form.volcengine_secret_access_key.placeholder": "Please enter VolcEngine SecretAccessKey",
"access.form.volcengine_secret_access_key.tooltip": "For more information, see <a href=\"https://www.volcengine.com/docs/6291/216571\" target=\"_blank\">https://www.volcengine.com/docs/6291/216571</a>",
"access.form.wangsu_access_key_id.label": "Wangsu Cloud AccessKeyId",
"access.form.wangsu_access_key_id.placeholder": "Please enter Wangsu Cloud AccessKeyId",
"access.form.wangsu_access_key_id.tooltip": "For more information, see <a href=\"https://en.wangsu.com/document/account-manage/15775\" target=\"_blank\">https://en.wangsu.com/document/account-manage/15775</a>",
"access.form.wangsu_access_key_secret.label": "Wangsu Cloud AccessKeySecret",
"access.form.wangsu_access_key_secret.placeholder": "Please enter Wangsu Cloud AccessKeySecret",
"access.form.wangsu_access_key_secret.tooltip": "For more information, see <a href=\"https://en.wangsu.com/document/account-manage/15775\" target=\"_blank\">https://en.wangsu.com/document/account-manage/15775</a>",
"access.form.webhook_url.label": "Webhook URL",
"access.form.webhook_url.placeholder": "Please enter Webhook URL",
"access.form.webhook_allow_insecure_conns.label": "Insecure SSL/TLS connections",

View File

@ -41,6 +41,8 @@
"common.notifier.email": "Email",
"common.notifier.gotify": "Gotify",
"common.notifier.lark": "Lark",
"common.notifier.mattermost": "Mattermost",
"common.notifier.pushover": "Pushover",
"common.notifier.pushplus": "PushPlus",
"common.notifier.serverchan": "ServerChan",
"common.notifier.telegram": "Telegram",

View File

@ -5,7 +5,7 @@
"provider.acmehttpreq": "Http Request (ACME Proxy)",
"provider.aliyun": "Alibaba Cloud",
"provider.aliyun.alb": "Alibaba Cloud - ALB (Application Load Balancer)",
"provider.aliyun.cas": "Alibaba Cloud - Upload to CAS (Certificate Management Service)",
"provider.aliyun.cas_upload": "Alibaba Cloud - Upload to CAS (Certificate Management Service)",
"provider.aliyun.cas_deploy": "Alibaba Cloud - Deploy via CAS (Certificate Management Service)",
"provider.aliyun.cdn": "Alibaba Cloud - CDN (Content Delivery Network)",
"provider.aliyun.clb": "Alibaba Cloud - CLB (Classic Load Balancer)",
@ -31,7 +31,7 @@
"provider.baiducloud.appblb": "Baidu Cloud - AppBLB (Application Baidu Load Balancer)",
"provider.baiducloud.blb": "Baidu Cloud - BLB (Baidu Load Balancer)",
"provider.baiducloud.cdn": "Baidu Cloud - CDN (Content Delivery Network)",
"provider.baiducloud.cert": "Baidu Cloud - Upload to SSL Certificate Service",
"provider.baiducloud.cert_upload": "Baidu Cloud - Upload to SSL Certificate Service",
"provider.baiducloud.dns": "Baidu Cloud - DNS (Domain Name Service)",
"provider.baishan": "Baishan",
"provider.baishan.cdn": "Baishan - CDN (Content Delivery Network)",
@ -67,7 +67,7 @@
"provider.huaweicloud.cdn": "Huawei Cloud - CDN (Content Delivery Network)",
"provider.huaweicloud.dns": "Huawei Cloud - DNS (Domain Name Service)",
"provider.huaweicloud.elb": "Huawei Cloud - ELB (Elastic Load Balance)",
"provider.huaweicloud.scm": "Huawei Cloud - Upload to SCM (SSL Certificate Manager)",
"provider.huaweicloud.scm_upload": "Huawei Cloud - Upload to SCM (SSL Certificate Manager)",
"provider.huaweicloud.waf": "Huawei Cloud - WAF (Web Application Firewall)",
"provider.jdcloud": "JD Cloud",
"provider.jdcloud.alb": "JD Cloud - ALB (Application Load Balancer)",
@ -91,6 +91,7 @@
"provider.qiniu.kodo": "Qiniu - Kodo",
"provider.qiniu.pili": "Qiniu - Pili",
"provider.rainyun": "Rain Yun",
"provider.rainyun.rcdn": "Rain Yun - RCDN (Rain Content Delivery Network)",
"provider.safeline": "SafeLine",
"provider.ssh": "SSH deployment",
"provider.sslcom": "SSL.com",
@ -103,7 +104,7 @@
"provider.tencentcloud.ecdn": "Tencent Cloud - ECDN (Enterprise Content Delivery Network)",
"provider.tencentcloud.eo": "Tencent Cloud - EdgeOne",
"provider.tencentcloud.scf": "Tencent Cloud - SCF (Serverless Cloud Function)",
"provider.tencentcloud.ssl": "Tencent Cloud - Upload to SSL Certificate Service",
"provider.tencentcloud.ssl_upload": "Tencent Cloud - Upload to SSL Certificate Service",
"provider.tencentcloud.ssl_deploy": "Tencent Cloud - Deploy via SSL Certificate Service",
"provider.tencentcloud.vod": "Tencent Cloud - VOD (Video on Demand)",
"provider.tencentcloud.waf": "Tencent Cloud - WAF (Web Application Firewall)",
@ -112,18 +113,20 @@
"provider.ucloud.us3": "UCloud - US3 (UCloud Object-based Storage)",
"provider.upyun": "UPYUN",
"provider.upyun.cdn": "UPYUN - CDN (Content Delivery Network)",
"provider.upyun.file": "UPYUN - File Storage",
"provider.upyun.file": "UPYUN - USS (Storage Service)",
"provider.vercel": "Vercel",
"provider.volcengine": "Volcengine",
"provider.volcengine.alb": "Volcengine - ALB (Application Load Balancer)",
"provider.volcengine.cdn": "Volcengine - CDN (Content Delivery Network)",
"provider.volcengine.certcenter": "Volcengine - Upload to Certificate Center",
"provider.volcengine.certcenter_upload": "Volcengine - Upload to Certificate Center",
"provider.volcengine.clb": "Volcengine - CLB (Cloud Load Balancer)",
"provider.volcengine.dcdn": "Volcengine - DCDN (Dynamic Content Delivery Network)",
"provider.volcengine.dns": "Volcengine - DNS (Domain Name Service)",
"provider.volcengine.imagex": "Volcengine - ImageX",
"provider.volcengine.live": "Volcengine - Live",
"provider.volcengine.tos": "Volcengine - TOS (Tinder Object Storage)",
"provider.wangsu": "Wangsu Cloud",
"provider.wangsu.cdnpro": "Wangsu Cloud - CDN Pro",
"provider.webhook": "Webhook",
"provider.westcn": "West.cn",
"provider.zerossl": "ZeroSSL",

View File

@ -53,21 +53,38 @@
"settings.notification.channel.form.email_sender_address.placeholder": "Please enter sender email address",
"settings.notification.channel.form.email_receiver_address.label": "Receiver email address",
"settings.notification.channel.form.email_receiver_address.placeholder": "Please enter receiver email address",
"settings.notification.channel.form.gotify_url.placeholder": "Please enter Service URL",
"settings.notification.channel.form.gotify_url.label": "Service URL",
"settings.notification.channel.form.gotify_url.tooltip": "Example: <b>https://gotify.exmaple.com</b>, the protocol needs to be included but the trailing '/' should not be included.<br>For more information, see <a href=\"https://gotify.net/docs/pushmsg\" target=\"_blank\">https://gotify.net/docs/pushmsg</a>",
"settings.notification.channel.form.gotify_token.placeholder": "Please enter Application Token",
"settings.notification.channel.form.gotify_token.label": "Application Token",
"settings.notification.channel.form.gotify_url.placeholder": "Please enter Service URL",
"settings.notification.channel.form.gotify_url.tooltip": "For more information, see <a href=\"https://gotify.net/docs/pushmsg\" target=\"_blank\">https://gotify.net/docs/pushmsg</a><br><br>Example: <b>https://gotify.exmaple.com</b>, the trailing '/' should not be included.",
"settings.notification.channel.form.gotify_token.label": "Application token",
"settings.notification.channel.form.gotify_token.placeholder": "Please enter Application token",
"settings.notification.channel.form.gotify_token.tooltip": "For more information, see <a href=\"https://gotify.net/docs/pushmsg\" target=\"_blank\">https://gotify.net/docs/pushmsg</a>",
"settings.notification.channel.form.gotify_priority.placeholder": "Please enter message priority",
"settings.notification.channel.form.gotify_priority.label": "Message Priority",
"settings.notification.channel.form.gotify_priority.tooltip": "Message Priority, you can set it to 1 as default.<br>For more information, see <a href=\"https://gotify.net/docs/pushmsg\" target=\"_blank\">https://gotify.net/docs/pushmsg</a><br><a href=\"https://github.com/gotify/android/issues/18#issuecomment-437403888\" target=\"_blank\">https://github.com/gotify/android/issues/18#issuecomment-437403888</a>",
"settings.notification.channel.form.gotify_priority.label": "Message priority",
"settings.notification.channel.form.gotify_priority.tooltip": "For more information, see <a href=\"https://gotify.net/docs/pushmsg\" target=\"_blank\">https://gotify.net/docs/pushmsg</a>, <a href=\"https://github.com/gotify/android/issues/18#issuecomment-437403888\" target=\"_blank\">https://github.com/gotify/android/issues/18#issuecomment-437403888</a>",
"settings.notification.channel.form.gotify_priority.error.gte0": "Message Priority must be greater than or equal to 0.",
"settings.notification.channel.form.lark_webhook_url.label": "Webhook URL",
"settings.notification.channel.form.lark_webhook_url.placeholder": "Please enter Webhook URL",
"settings.notification.channel.form.lark_webhook_url.tooltip": "For more information, see <a href=\"https://www.feishu.cn/hc/en-US/articles/807992406756\" target=\"_blank\">https://www.feishu.cn/hc/en-US/articles/807992406756</a>",
"settings.notification.channel.form.mattermost_server_url.label": "Service URL",
"settings.notification.channel.form.mattermost_server_url.placeholder": "Please enter service URL",
"settings.notification.channel.form.mattermost_server_url.tooltip": "Example: <b>https://exmaple.com</b>, the protocol needs to be included but the trailing '/' should not be included.",
"settings.notification.channel.form.mattermost_channel_id.label": "Channel ID",
"settings.notification.channel.form.mattermost_channel_id.placeholder": "Please enter channel ID",
"settings.notification.channel.form.mattermost_channel_id.tooltip": "How to get the channel ID? Select the target channel from the left sidebar, click on the channel name at the top, and choose ”Channel Details.” You can directly see the channel ID on the pop-up page.",
"settings.notification.channel.form.mattermost_username.label": "Username",
"settings.notification.channel.form.mattermost_username.placeholder": "Please enter username",
"settings.notification.channel.form.mattermost_password.label": "Password",
"settings.notification.channel.form.mattermost_password.placeholder": "Please enter password",
"settings.notification.channel.form.pushover_token.placeholder": "Please enter Application API Token",
"settings.notification.channel.form.pushover_token.label": "Application API Token",
"settings.notification.channel.form.pushover_token.tooltip": "For more information, see <a href=\"https://pushover.net/api#registration\" target=\"_blank\">https://pushover.net/api#registration</a>",
"settings.notification.channel.form.pushover_user.placeholder": "Please enter User/Group Key",
"settings.notification.channel.form.pushover_user.label": "User/Group Key",
"settings.notification.channel.form.pushover_user.tooltip": "For more information, see <a href=\"https://pushover.net/api#identifiers\" target=\"_blank\">https://pushover.net/api#identifiers</a>",
"settings.notification.channel.form.pushplus_token.placeholder": "Please enter Token",
"settings.notification.channel.form.pushplus_token.label": "Token",
"settings.notification.channel.form.pushplus_token.placeholder": "Please enter token",
"settings.notification.channel.form.pushplus_token.tooltip": "For more information, see <a href=\"https://www.pushplus.plus/push1.html\" target=\"_blank\">https://www.pushplus.plus/push1.html</a>",
"settings.notification.channel.form.serverchan_url.label": "Server URL",
"settings.notification.channel.form.serverchan_url.placeholder": "Please enter ServerChan server URL (e.g. https://sctapi.ftqq.com/*****.send)",

View File

@ -95,7 +95,7 @@
"workflow_node.deploy.form.provider.placeholder": "Please select deploy target",
"workflow_node.deploy.form.provider_access.label": "Host provider authorization",
"workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of host provider",
"workflow_node.deploy.form.provider_access.tooltip": "Used to deploy certificates.",
"workflow_node.deploy.form.provider_access.tooltip": "Used to invoke API during deployment.",
"workflow_node.deploy.form.provider_access.button": "Create",
"workflow_node.deploy.form.provider_access.guide_for_local": "Tips: If you are running Certimate in Docker, the \"Local\" refers to the container rather than the host.",
"workflow_node.deploy.form.certificate.label": "Certificate",
@ -269,11 +269,11 @@
"workflow_node.deploy.form.baiducloud_cdn_domain.label": "Baidu Cloud CDN domain",
"workflow_node.deploy.form.baiducloud_cdn_domain.placeholder": "Please enter Baidu Cloud CDN domain name",
"workflow_node.deploy.form.baiducloud_cdn_domain.tooltip": "For more information, see <a href=\"https://console.bce.baidu.com/cdn\" target=\"_blank\">https://console.bce.baidu.com/cdn</a>",
"workflow_node.deploy.form.baishan_cdn_domain.label": "Baishan CDN domain",
"workflow_node.deploy.form.baishan_cdn_domain.placeholder": "Please enter Baishan CDN domain name",
"workflow_node.deploy.form.baishan_cdn_domain.label": "Baishan Cloud CDN domain",
"workflow_node.deploy.form.baishan_cdn_domain.placeholder": "Please enter Baishan Cloud CDN domain name",
"workflow_node.deploy.form.baishan_cdn_domain.tooltip": "For more information, see <a href=\"https://cdnx.console.baishan.com\" target=\"_blank\">https://cdnx.console.baishan.com</a>",
"workflow_node.deploy.form.baishan_cdn_certificate_id.label": "Baishan CDN certificate ID (Optional",
"workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder": "Please enter Baishan CDN certificate ID",
"workflow_node.deploy.form.baishan_cdn_certificate_id.label": "Baishan Cloud CDN certificate ID (Optional)",
"workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder": "Please enter Baishan Cloud CDN certificate ID",
"workflow_node.deploy.form.baishan_cdn_certificate_id.tooltip": "For more information, see <a href=\"https://cdnx.console.baishan.com/#/cdn/cert\" target=\"_blank\">https://cdnx.console.baishan.com/#/cdn/cert</a>",
"workflow_node.deploy.form.baotapanel_console_auto_restart.label": "Auto restart after deployment",
"workflow_node.deploy.form.baotapanel_site_type.label": "aaPanel site type",
@ -436,6 +436,12 @@
"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 <a href=\"hhttps://portal.qiniu.com/hub\" target=\"_blank\">https://portal.qiniu.com/hub</a>",
"workflow_node.deploy.form.rainyun_rcdn_instance_id.label": "Rain Yun RCDN instance ID",
"workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder": "Please enter Rain Yun RCDN instance ID",
"workflow_node.deploy.form.rainyun_rcdn_instance_id.tooltip": "For more information, see <a href=\"https://app.rainyun.com/apps/rcdn/list\" target=\"_blank\">https://app.rainyun.com/apps/rcdn/list</a>",
"workflow_node.deploy.form.rainyun_rcdn_domain.label": "Rain Yun RCDN domain",
"workflow_node.deploy.form.rainyun_rcdn_domain.placeholder": "Please enter Rain Yun RCDN domain name",
"workflow_node.deploy.form.rainyun_rcdn_domain.tooltip": "For more information, see <a href=\"https://app.rainyun.com/apps/rcdn/list\" target=\"_blank\">https://app.rainyun.com/apps/rcdn/list</a>",
"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",
@ -633,6 +639,19 @@
"workflow_node.deploy.form.volcengine_tos_domain.label": "VolcEngine TOS domain",
"workflow_node.deploy.form.volcengine_tos_domain.placeholder": "Please enter VolcEngine TOS domain name",
"workflow_node.deploy.form.volcengine_tos_domain.tooltip": "For more information, see <a href=\"https://console.volcengine.com/tos\" target=\"_blank\">https://console.volcengine.com/tos</a>",
"workflow_node.deploy.form.wangsu_cdnpro_environment.label": "Wangsu Cloud environment",
"workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder": "Please select Wangsu Cloud environment",
"workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label": "Production environment",
"workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label": "Staging environment",
"workflow_node.deploy.form.wangsu_cdnpro_domain.label": "Wangsu Cloud CDN domain",
"workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder": "Please enter Wangsu Cloud CDN domain name",
"workflow_node.deploy.form.wangsu_cdnpro_domain.tooltip": "For more information, see <a href=\"https://cdnpro.console.wangsu.com/v2/index/#/properties\" target=\"_blank\">https://cdnpro.console.wangsu.com/v2/index/#/properties</a>",
"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label": "Wangsu Cloud CDN certificate ID (Optional)",
"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder": "Please enter Wangsu Cloud CDN certificate ID",
"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip": "For more information, see <a href=\"https://cdnpro.console.wangsu.com/v2/index/#/certificate\" target=\"_blank\">https://cdnpro.console.wangsu.com/v2/index/#/certificate</a>",
"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label": "Wangsu Cloud CDN Webhook ID (Optional)",
"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder": "Please enter Wangsu Cloud CDN Webhook ID",
"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip": "For more information, see <a href=\"https://cdnpro.console.wangsu.com/v2/index/#/certificate\" target=\"_blank\">https://cdnpro.console.wangsu.com/v2/index/#/certificate</a>",
"workflow_node.deploy.form.webhook_data.label": "Webhook data (JSON format)",
"workflow_node.deploy.form.webhook_data.placeholder": "Please enter Webhook data",
"workflow_node.deploy.form.webhook_data.guide": "Tips: The Webhook data should be a key-value pair in JSON format. The values in JSON support template variables, which will be replaced by actual values when sent to the Webhook URL. <br><br>Supported variables: <br><strong>${DOMAIN}</strong>: The primary domain of the certificate (<i>CommonName</i>).<br><strong>${DOMAINS}</strong>: The domain list of the certificate (<i>SubjectAltNames</i>).<br><strong>${CERTIFICATE}</strong>: The PEM format content of the certificate file.<br><strong>${PRIVATE_KEY}</strong>: The PEM format content of the private key file.",

View File

@ -5,6 +5,8 @@
"workflow_run.action.delete": "Delete run",
"workflow_run.action.delete.confirm": "Are you sure to delete this run?",
"workflow_run.table.alert": "Attention: The workflow run contains the execution results of each node. Deleting it may trigger re-application or re-deployment of certificates due to the inability to find the previous execution result. Please do not delete unless necessary. It is recommended to keep it for at least 180 days.",
"workflow_run.props.id": "ID",
"workflow_run.props.status": "Status",
"workflow_run.props.status.pending": "Pending",

View File

@ -17,6 +17,7 @@
"access.props.provider.usage.hosting": "主机提供商",
"access.props.provider.usage.ca": "证书颁发机构",
"access.props.provider.usage.notification": "通知渠道",
"access.props.provider.builtin": "内置",
"access.props.range.both_dns_hosting": "提供商",
"access.props.range.ca_only": "证书颁发机构",
"access.props.range.notify_only": "通知渠道",
@ -231,7 +232,7 @@
"access.form.qiniu_secret_key.tooltip": "这是什么?请参阅 <a href=\"https://portal.qiniu.com/\" target=\"_blank\">https://portal.qiniu.com/</a>",
"access.form.rainyun_api_key.label": "雨云 API 密钥",
"access.form.rainyun_api_key.placeholder": "请输入雨云 API 密钥",
"access.form.rainyun_api_key.tooltip": "这是什么?请参阅 <a href=\"https://www.rainyun.com/docs/account/racc/setting#api%E5%AF%86%E9%92%A5\" target=\"_blank\">https://www.rainyun.com/docs/account/racc/setting</a>",
"access.form.rainyun_api_key.tooltip": "这是什么?请参阅 <a href=\"https://app.rainyun.com/account/settings/api-key\" target=\"_blank\">https://app.rainyun.com/account/settings/api-key</a>",
"access.form.safeline_api_url.label": "雷池 URL",
"access.form.safeline_api_url.placeholder": "请输入雷池 URL",
"access.form.safeline_api_url.tooltip": "这是什么?请参阅 <a href=\"https://docs.waf-ce.chaitin.cn/zh/%E4%B8%8A%E6%89%8B%E6%8C%87%E5%8D%97/%E5%AE%89%E8%A3%85%E9%9B%B7%E6%B1%A0#%E8%AE%BF%E9%97%AE%E9%9B%B7%E6%B1%A0%E6%8E%A7%E5%88%B6%E5%8F%B0\" target=\"_blank\">https://docs.waf-ce.chaitin.cn/zh/上手指南/安装雷池</a>",
@ -297,6 +298,12 @@
"access.form.volcengine_secret_access_key.label": "火山引擎 SecretAccessKey",
"access.form.volcengine_secret_access_key.placeholder": "请输入火山引擎 SecretAccessKey",
"access.form.volcengine_secret_access_key.tooltip": "这是什么?请参阅 <a href=\"https://www.volcengine.com/docs/6291/216571\" target=\"_blank\">https://www.volcengine.com/docs/6291/216571</a>",
"access.form.wangsu_access_key_id.label": "网宿云 AccessKeyId",
"access.form.wangsu_access_key_id.placeholder": "请输入网宿科技 AccessKeyId",
"access.form.wangsu_access_key_id.tooltip": "这是什么?请参阅 <a href=\"https://www.wangsu.com/document/account-manage/15775\" target=\"_blank\">https://www.wangsu.com/document/account-manage/15775</a>",
"access.form.wangsu_access_key_secret.label": "网宿科技 AccessKeySecret",
"access.form.wangsu_access_key_secret.placeholder": "请输入网宿科技 AccessKeySecret",
"access.form.wangsu_access_key_secret.tooltip": "这是什么?请参阅 <a href=\"https://www.wangsu.com/document/account-manage/15775\" target=\"_blank\">https://www.wangsu.com/document/account-manage/15775</a>",
"access.form.webhook_url.label": "Webhook 回调地址",
"access.form.webhook_url.placeholder": "请输入 Webhook 回调地址",
"access.form.webhook_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误",

View File

@ -41,6 +41,8 @@
"common.notifier.email": "邮件",
"common.notifier.gotify": "Gotify",
"common.notifier.lark": "飞书",
"common.notifier.mattermost": "Mattermost",
"common.notifier.pushover": "Pushover",
"common.notifier.pushplus": "PushPlus推送加",
"common.notifier.serverchan": "Server 酱",
"common.notifier.telegram": "Telegram",

View File

@ -5,7 +5,7 @@
"provider.acmehttpreq": "Http Request (ACME Proxy)",
"provider.aliyun": "阿里云",
"provider.aliyun.alb": "阿里云 - 应用型负载均衡 ALB",
"provider.aliyun.cas": "阿里云 - 上传到数字证书管理服务 CAS",
"provider.aliyun.cas_upload": "阿里云 - 上传到数字证书管理服务 CAS",
"provider.aliyun.cas_deploy": "阿里云 - 通过数字证书管理服务 CAS 创建部署任务",
"provider.aliyun.cdn": "阿里云 - 内容分发网络 CDN",
"provider.aliyun.clb": "阿里云 - 传统型负载均衡 CLB",
@ -31,7 +31,7 @@
"provider.baiducloud.appblb": "百度智能云 - 应用型负载均衡 BLB",
"provider.baiducloud.blb": "百度智能云 - 普通型负载均衡 BLB",
"provider.baiducloud.cdn": "百度智能云 - 内容分发网络 CDN",
"provider.baiducloud.cert": "百度智能云 - 上传到 SSL 证书服务",
"provider.baiducloud.cert_upload": "百度智能云 - 上传到 SSL 证书服务",
"provider.baiducloud.dns": "百度智能云 - 智能云解析 DNS",
"provider.baishan": "白山云",
"provider.baishan.cdn": "白山云 - 内容分发网络 CDN",
@ -67,7 +67,7 @@
"provider.huaweicloud.cdn": "华为云 - 内容分发网络 CDN",
"provider.huaweicloud.dns": "华为云 - 云解析 DNS",
"provider.huaweicloud.elb": "华为云 - 弹性负载均衡 ELB",
"provider.huaweicloud.scm": "华为云 - 上传到云证书管理服务 SCM",
"provider.huaweicloud.scm_upload": "华为云 - 上传到云证书管理服务 SCM",
"provider.huaweicloud.waf": "华为云 - Web 应用防火墙 WAF",
"provider.jdcloud": "京东云",
"provider.jdcloud.alb": "京东云 - 应用负载均衡 ALB",
@ -91,6 +91,7 @@
"provider.qiniu.kodo": "七牛云 - 对象存储 Kodo",
"provider.qiniu.pili": "七牛云 - 视频直播 Pili",
"provider.rainyun": "雨云",
"provider.rainyun.rcdn": "雨云 - 雨盾 CDN",
"provider.safeline": "雷池",
"provider.ssh": "SSH 部署",
"provider.sslcom": "SSL.com",
@ -103,7 +104,7 @@
"provider.tencentcloud.ecdn": "腾讯云 - 全站加速网络 ECDN",
"provider.tencentcloud.eo": "腾讯云 - 边缘安全加速平台 EdgeOne",
"provider.tencentcloud.scf": "腾讯云 - 云函数 SCF",
"provider.tencentcloud.ssl": "腾讯云 - 上传到 SSL 证书服务",
"provider.tencentcloud.ssl_upload": "腾讯云 - 上传到 SSL 证书服务",
"provider.tencentcloud.ssl_deploy": "腾讯云 - 通过 SSL 证书服务创建部署任务",
"provider.tencentcloud.vod": "腾讯云 - 云点播 VOD",
"provider.tencentcloud.waf": "腾讯云 - Web 应用防火墙 WAF",
@ -112,18 +113,20 @@
"provider.ucloud.us3": "优刻得 - 对象存储 US3",
"provider.upyun": "又拍云",
"provider.upyun.cdn": "又拍云 - 云分发 CDN",
"provider.upyun.file": "又拍云 - 云存储",
"provider.upyun.file": "又拍云 - 云存储 USS",
"provider.vercel": "Vercel",
"provider.volcengine": "火山引擎",
"provider.volcengine.alb": "火山引擎 - 应用型负载均衡 ALB",
"provider.volcengine.cdn": "火山引擎 - 内容分发网络 CDN",
"provider.volcengine.certcenter": "火山引擎 - 上传到证书中心",
"provider.volcengine.certcenter_upload": "火山引擎 - 上传到证书中心",
"provider.volcengine.clb": "火山引擎 - 负载均衡 CLB",
"provider.volcengine.dcdn": "火山引擎 - 全站加速 DCDN",
"provider.volcengine.dns": "火山引擎 - 云解析 DNS",
"provider.volcengine.imagex": "火山引擎 - 图片服务 ImageX",
"provider.volcengine.live": "火山引擎 - 视频直播 Live",
"provider.volcengine.tos": "火山引擎 - 对象存储 TOS",
"provider.wangsu": "网宿云",
"provider.wangsu.cdnpro": "网宿云 - CDN Pro",
"provider.webhook": "Webhook",
"provider.westcn": "西部数码",
"provider.zerossl": "ZeroSSL",

View File

@ -53,22 +53,39 @@
"settings.notification.channel.form.email_sender_address.placeholder": "请输入发送邮箱地址",
"settings.notification.channel.form.email_receiver_address.label": "接收邮箱地址",
"settings.notification.channel.form.email_receiver_address.placeholder": "请输入接收邮箱地址",
"settings.notification.channel.form.gotify_url.placeholder": "请输入服务地址",
"settings.notification.channel.form.gotify_url.label": "服务地址",
"settings.notification.channel.form.gotify_url.tooltip": "示例: <b>https://gotify.exmaple.com</>,需要包含协议但不要包含末尾的'/'。<br>请参阅 <a href=\"https://gotify.net/docs/pushmsg\" target=\"_blank\">https://gotify.net/docs/pushmsg</a>",
"settings.notification.channel.form.gotify_token.placeholder": "请输入应用Token",
"settings.notification.channel.form.gotify_url.placeholder": "请输入服务地址",
"settings.notification.channel.form.gotify_url.tooltip": "这是什么?请参阅 <a href=\"https://gotify.net/docs/pushmsg\" target=\"_blank\">https://gotify.net/docs/pushmsg</a><br><br>示例: <b>https://gotify.exmaple.com</b>,不要包含末尾的'/'。",
"settings.notification.channel.form.gotify_token.label": "应用 Token",
"settings.notification.channel.form.gotify_token.tooltip": "应用Token。<br>请参阅 <a href=\"https://gotify.net/docs/pushmsg\" target=\"_blank\">https://gotify.net/docs/pushmsg</a>",
"settings.notification.channel.form.gotify_token.placeholder": "请输入应用 Token",
"settings.notification.channel.form.gotify_token.tooltip": "这是什么?请参阅 <a href=\"https://gotify.net/docs/pushmsg\" target=\"_blank\">https://gotify.net/docs/pushmsg</a>",
"settings.notification.channel.form.gotify_priority.label": "消息优先级(可选)",
"settings.notification.channel.form.gotify_priority.placeholder": "请输入消息优先级",
"settings.notification.channel.form.gotify_priority.label": "消息优先级",
"settings.notification.channel.form.gotify_priority.tooltip": "消息优先级, 可以设置为1作为默认值。<br>请参阅 <a href=\"https://gotify.net/docs/pushmsg\" target=\"_blank\">https://gotify.net/docs/pushmsg</a><br><a href=\"https://github.com/gotify/android/issues/18#issuecomment-437403888\" target=\"_blank\">https://github.com/gotify/android/issues/18#issuecomment-437403888</a>",
"settings.notification.channel.form.gotify_priority.tooltip": "这是什么?请参阅 <a href=\"https://gotify.net/docs/pushmsg\" target=\"_blank\">https://gotify.net/docs/pushmsg</a>、<a href=\"https://github.com/gotify/android/issues/18#issuecomment-437403888\" target=\"_blank\">https://github.com/gotify/android/issues/18#issuecomment-437403888</a>",
"settings.notification.channel.form.gotify_priority.error.gte0": "消息优先级需要大于等于 0",
"settings.notification.channel.form.lark_webhook_url.label": "机器人 Webhook 地址",
"settings.notification.channel.form.lark_webhook_url.placeholder": "请输入机器人 Webhook 地址",
"settings.notification.channel.form.lark_webhook_url.tooltip": "这是什么?请参阅 <a href=\"https://www.feishu.cn/hc/zh-CN/articles/807992406756\" target=\"_blank\">https://www.feishu.cn/hc/zh-CN/articles/807992406756</a>",
"settings.notification.channel.form.mattermost_server_url.label": "服务地址",
"settings.notification.channel.form.mattermost_server_url.placeholder": "请输入服务地址",
"settings.notification.channel.form.mattermost_server_url.tooltip": "示例: <b>https://exmaple.com</b>,需要包含协议但不要包含末尾的'/'",
"settings.notification.channel.form.mattermost_channel_id.label": "频道ID",
"settings.notification.channel.form.mattermost_channel_id.placeholder": "请输入频道ID",
"settings.notification.channel.form.mattermost_channel_id.tooltip": "频道ID怎么获取从左侧边栏中选择目标频道点击顶部的频道名称选择“频道详情”即可在弹出页面中直接看到频道ID",
"settings.notification.channel.form.mattermost_username.label": "用户名",
"settings.notification.channel.form.mattermost_username.placeholder": "请输入用户名",
"settings.notification.channel.form.mattermost_password.label": "密码",
"settings.notification.channel.form.mattermost_password.placeholder": "请输入密码",
"settings.notification.channel.form.pushover_token.placeholder": "请输入应用 API Token",
"settings.notification.channel.form.pushover_token.label": "应用 API Token",
"settings.notification.channel.form.pushover_token.tooltip": "这是什么?请参阅 <a href=\"https://pushover.net/api#registration\" target=\"_blank\">https://pushover.net/api#registration</a>",
"settings.notification.channel.form.pushover_user.placeholder": "请输入用户/分组 Key",
"settings.notification.channel.form.pushover_user.label": "用户/分组 Key",
"settings.notification.channel.form.pushover_user.tooltip": "这是什么?请参阅 <a href=\"https://pushover.net/api#identifiers\" target=\"_blank\">https://pushover.net/api#identifiers</a>",
"settings.notification.channel.form.pushplus_token.placeholder": "请输入Token",
"settings.notification.channel.form.pushplus_token.label": "Token",
"settings.notification.channel.form.pushplus_token.tooltip": "请参阅 <a href=\"https://www.pushplus.plus/push1.html\" target=\"_blank\">https://www.pushplus.plus/push1.html</a>",
"settings.notification.channel.form.pushplus_token.placeholder": "请输入 Token",
"settings.notification.channel.form.pushplus_token.tooltip": "这是什么?请参阅 <a href=\"https://www.pushplus.plus/push1.html\" target=\"_blank\">https://www.pushplus.plus/push1.html</a>",
"settings.notification.channel.form.serverchan_url.label": "服务器地址",
"settings.notification.channel.form.serverchan_url.placeholder": "请输入服务器地址(形如: https://sctapi.ftqq.com/*****.send",
"settings.notification.channel.form.serverchan_url.tooltip": "这是什么?请参阅 <a href=\"https://sct.ftqq.com/forward\" target=\"_blank\">https://sct.ftqq.com/forward</a>",

View File

@ -94,7 +94,7 @@
"workflow_node.deploy.form.provider.placeholder": "请选择部署目标",
"workflow_node.deploy.form.provider_access.label": "主机提供商授权",
"workflow_node.deploy.form.provider_access.placeholder": "请选择主机提供商授权",
"workflow_node.deploy.form.provider_access.tooltip": "用于部署证书,注意与申请阶段所需的 DNS 提供商相区分。",
"workflow_node.deploy.form.provider_access.tooltip": "用于部署证书时调用相关 API,注意与申请阶段所需的 DNS 提供商相区分。",
"workflow_node.deploy.form.provider_access.button": "新建",
"workflow_node.deploy.form.provider_access.guide_for_local": "小贴士:如果你正在使用 Docker 运行 Certimate“本地”指的是容器内而非宿主机。",
"workflow_node.deploy.form.certificate.label": "待部署证书",
@ -435,6 +435,12 @@
"workflow_node.deploy.form.qiniu_pili_domain.label": "七牛云视频直播流域名",
"workflow_node.deploy.form.qiniu_pili_domain.placeholder": "请输入七牛云视频直播流域名",
"workflow_node.deploy.form.qiniu_pili_domain.tooltip": "这是什么?请参阅 <a href=\"hhttps://portal.qiniu.com/hub\" target=\"_blank\">https://portal.qiniu.com/hub</a>",
"workflow_node.deploy.form.rainyun_rcdn_instance_id.label": "雨云 RCDN 实例 ID",
"workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder": "请输入雨云 RCDN 实例 ID",
"workflow_node.deploy.form.rainyun_rcdn_instance_id.tooltip": "这是什么?请参阅 <a href=\"https://app.rainyun.com/apps/rcdn/list\" target=\"_blank\">https://app.rainyun.com/apps/rcdn/list</a>",
"workflow_node.deploy.form.rainyun_rcdn_domain.label": "雨云 RCDN 加速域名",
"workflow_node.deploy.form.rainyun_rcdn_domain.placeholder": "请输入雨云 RCDN 加速域名(支持泛域名)",
"workflow_node.deploy.form.rainyun_rcdn_domain.tooltip": "这是什么?请参阅 <a href=\"https://app.rainyun.com/apps/rcdn/list\" target=\"_blank\">https://app.rainyun.com/apps/rcdn/list</a>",
"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": "替换指定证书",
@ -632,6 +638,19 @@
"workflow_node.deploy.form.volcengine_tos_domain.label": "火山引擎 TOS 自定义域名",
"workflow_node.deploy.form.volcengine_tos_domain.placeholder": "请输入火山引擎 TOS 自定义域名",
"workflow_node.deploy.form.volcengine_tos_domain.tooltip": "这是什么?请参阅 see <a href=\"https://console.volcengine.com/tos\" target=\"_blank\">https://console.volcengine.com/tos</a>",
"workflow_node.deploy.form.wangsu_cdnpro_environment.label": "网宿云环境",
"workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder": "请选择网宿云环境",
"workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label": "生产环境",
"workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label": "演练环境",
"workflow_node.deploy.form.wangsu_cdnpro_domain.label": "网宿云 CDN Pro 加速域名",
"workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder": "请输入网宿云 CDN Pro 加速域名(支持泛域名)",
"workflow_node.deploy.form.wangsu_cdnpro_domain.tooltip": "这是什么?请参阅 <a href=\"https://cdnpro.console.wangsu.com/v2/index/#/properties\" target=\"_blank\">https://cdnpro.console.wangsu.com/v2/index/#/properties</a>",
"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label": "网宿云 CDN Pro 原证书 ID可选",
"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder": "请输入网宿云 CDN Pro 原证书 ID",
"workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip": "这是什么?请参阅 <a href=\"https://cdnpro.console.wangsu.com/v2/index/#/certificate\" target=\"_blank\">https://cdnpro.console.wangsu.com/v2/index/#/certificate</a><br><br>不填写时,将上传新证书;否则,将替换原证书。",
"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label": "网宿云 CDN Pro 部署任务 Webhook ID可选",
"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder": "请输入网宿云 CDN Pro 部署任务 Webhook ID",
"workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip": "这是什么?请参阅 <a href=\"https://cdnpro.console.wangsu.com/v2/index/#/certificate\" target=\"_blank\">https://cdnpro.console.wangsu.com/v2/index/#/certificate</a>",
"workflow_node.deploy.form.webhook_data.label": "Webhook 回调数据JSON 格式)",
"workflow_node.deploy.form.webhook_data.placeholder": "请输入 Webhook 回调数据",
"workflow_node.deploy.form.webhook_data.guide": "小贴士:回调数据是一个 JSON 格式的键值对。其中值支持模板变量,将在被发送到指定的 Webhook URL 时被替换为实际值;其他内容将保持原样。<br><br>支持的变量:<br><strong>${DOMAIN}</strong>:证书的主域名(即 <i>CommonName</i><br><strong>${DOMAINS}</strong>:证书的多域名列表(即 <i>SubjectAltNames</i><br><strong>${CERTIFICATE}</strong>:证书文件 PEM 格式内容<br><strong>${PRIVATE_KEY}</strong>:私钥文件 PEM 格式内容",
@ -639,7 +658,7 @@
"workflow_node.deploy.form.webhook_data_preset.button": "使用预设模板",
"workflow_node.deploy.form.strategy_config.label": "执行策略",
"workflow_node.deploy.form.skip_on_last_succeeded.label": "重复部署",
"workflow_node.deploy.form.skip_on_last_succeeded.prefix": "当上次部署相同证书成功时",
"workflow_node.deploy.form.skip_on_last_succeeded.prefix": "当上次部署相同证书成功时",
"workflow_node.deploy.form.skip_on_last_succeeded.suffix": "重新部署。",
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "跳过",
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "不跳过",

View File

@ -5,6 +5,8 @@
"workflow_run.action.delete": "删除执行",
"workflow_run.action.delete.confirm": "确定要删除此执行吗?请注意此操作仅清除日志历史,但不会影响签发的证书。",
"workflow_run.table.alert": "注意:执行记录中包含工作流各节点的执行结果,删除后可能导致因找不到前次执行结果而触发重新申请或部署证书。如无必要请勿提前删除,建议保留至少 180 天。",
"workflow_run.props.id": "ID",
"workflow_run.props.status": "状态",
"workflow_run.props.status.pending": "等待执行",

View File

@ -285,7 +285,7 @@ const Dashboard = () => {
}}
pagination={false}
rowKey={(record) => record.id}
scroll={{ x: "max(100%, 960px)" }}
scroll={{ x: "max(100%, 720px)" }}
size="small"
/>
</Card>
@ -310,7 +310,7 @@ const StatisticCard = ({
onClick?: () => void;
}) => {
return (
<Card className="size-full overflow-hidden" hoverable loading={loading} variant="borderless" onClick={onClick}>
<Card className="size-full overflow-hidden" hoverable loading={loading} bordered={false} onClick={onClick}>
<Space size="middle">
{icon}
<Statistic