Merge branch 'main' into 'next'

This commit is contained in:
Fu Diwei
2026-09-07 09:03:00 +08:00
93 changed files with 3987 additions and 662 deletions
+36
View File
@@ -0,0 +1,36 @@
name: "Issue Manager"
on:
schedule:
# Daily at 01:30 UTC (09:30 Beijing time)
- cron: "30 1 * * *"
issue_comment:
types:
- created
issues:
types:
- labeled
pull_request_target:
types:
- labeled
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
issue-manager:
runs-on: ubuntu-latest
steps:
- name: Manage issues with the "answered" label
uses: tiangolo/issue-manager@0.8.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
config: >
{
"answered": {
"delay": 604800,
"message": "This issue has been automatically closed because there has been no further reply from you for over 7 days since our last response. If your problem is not resolved, please feel free to reopen this issue or open a new one with more details.\n\n---\n\n此 Issue 已被自动关闭。在我们最后一次回复后已超过 7 天未收到您的进一步回复。如果您的问题仍未解决,欢迎重新打开此 Issue 或创建新的 Issue 并提供更多详细信息。"
}
}
+2 -2
View File
@@ -40,7 +40,7 @@ An open-source and free self-hosted SSL certificates ACME tool, automates the fu
- **Self-hosted**: Private deployment. All data is stored locally, to ensure data privacy and security.
- **Cross Platforms**: Compatible with various operating systems, including Windows/Linux/macOS.
- **Zero Dependencies**: No need to install databases, runtimes, or any frameworks. Ready to use out of the box.
- **Low Resource Usage**: Extremely lightweight, requiring only ~16 MB of memory.
- **Low Resource Usage**: Extremely lightweight, requiring only ~20 MB of memory.
- **Easy to Use**: Automates certificate management with a visual workflow — all with just a few simple configurations.
## 💡 Features
@@ -50,7 +50,7 @@ An open-source and free self-hosted SSL certificates ACME tool, automates the fu
- Supports DNS-01 challenge and HTTP-01 challenge both.
- Supports various certificate formats such as PEM, PFX, JKS.
- Supports more than 70+ domain registrars (e.g., AWS, Cloudflare, GoDaddy, Alibaba Cloud, Tencent Cloud, etc. [Check out full providers](https://docs.certimate.me/en-US/docs/reference/providers#supported-dns-providers)).
- Supports more than 150+ deployment destinations (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out full providers](https://docs.certimate.me/en-US/docs/reference/providers#supported-hosting-providers)).
- Supports more than 160+ deployment destinations (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out full providers](https://docs.certimate.me/en-US/docs/reference/providers#supported-hosting-providers)).
- Supports multiple notification channels including email, Discord, Slack, Telegram, DingTalk, Feishu, WeCom, and more.
- Supports multiple ACME CAs including Let's Encrypt, Actalis, Google Trust Services, SSL.com, ZeroSSL, and more.
- More features waiting to be discovered.
+3 -3
View File
@@ -18,7 +18,7 @@
</div>
<div align="center">
<a href="https://trendshift.io/repositories/23513?utm_source=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-23513" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/23513/daily" alt="certimate-go%2Fcertimate | Trendshift" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/23513?utm_source=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-23513" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/23513/weekly?language=Go" alt="certimate-go%2Fcertimate | Trendshift" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/23513?utm_source=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-23513" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/23513/monthly?language=Go" alt="certimate-go%2Fcertimate | Trendshift" width="250" height="55"/></a>
@@ -40,7 +40,7 @@
- **自托管**:私有化安装,所有数据本地化存储,掌控数据的隐私与安全。
- **跨平台**:Windows/Linux/macOS,全平台多架构可运行。
- **零依赖**:无需安装数据库、运行时或复杂框架,一键启动,开箱即用。
- **低占用**:超轻量的资源开销,仅需 ~16 MB 内存。
- **低占用**:超轻量的资源开销,仅需 ~20 MB 常驻内存。
- **易操作**:图形化界面,通过简单配置即可完成证书的自动化管理。
## 💡 功能特性
@@ -50,7 +50,7 @@
- 支持 DNS-01(即基于域名解析验证)、HTTP-01(即基于文件验证)两种质询方式。
- 支持 PEM、PFX、JKS 等多种格式输出证书。
- 支持 70+ 域名托管商(如阿里云、腾讯云、AWS、Cloudflare、GoDaddy 等,[点此查看完整清单](https://docs.certimate.me/zh-CN/docs/reference/providers#supported-dns-providers))。
- 支持 150+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等,[点此查看完整清单](https://docs.certimate.me/zh-CN/docs/reference/providers#supported-hosting-providers))。
- 支持 160+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等,[点此查看完整清单](https://docs.certimate.me/zh-CN/docs/reference/providers#supported-hosting-providers))。
- 支持邮件、钉钉、飞书、企业微信、Discord、Slack、Telegram 等多种通知渠道。
- 支持 Let's Encrypt、Actalis、Google Trust Services、SSL.com、ZeroSSL 等多种 ACME 证书颁发机构。
- 更多特性等待探索。
+1
View File
@@ -66,6 +66,7 @@ require (
github.com/pquerna/otp v1.5.0
github.com/samber/lo v1.53.0
github.com/spf13/cobra v1.10.2
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/antiddos v1.3.131
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.3.116
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.3.142
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.145
+3
View File
@@ -857,6 +857,8 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/antiddos v1.3.131 h1:rfaTxEAxIaVWHLIu9IzvBUKTW1PHBU3GWgYwj63pqFc=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/antiddos v1.3.131/go.mod h1:cLvVHLUt/aoJ3TLNQlNDFj2E1XWTcC+7ck7Lt+nA8pA=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.3.116 h1:pnWXFgtIgCIN+1OB5a/8+FX5/Yydg78DYQGooDmL32k=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.3.116/go.mod h1:/DjlZsmhK9WYAV4ByLrEdgzTuQLlOZISvA8a7wnsEh8=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.3.142 h1:3t++yffGVnGbLMg/ixQFYGBASOGXKTUHV+8kqSrywew=
@@ -867,6 +869,7 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.38/go.mod h
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.101/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.105/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.116/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.131/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.133/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.140/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.142/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
@@ -0,0 +1,27 @@
package deployers
import (
"fmt"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core"
dplyimpl "github.com/certimate-go/certimate/pkg/core/deployer/providers/asiaispcdn"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.DeploymentProviderTypeAsiaISPCDN, func(options *ProviderFactoryOptions) (core.Deployer, error) {
credentials := domain.AccessConfigForAsiaISPCDN{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{
AccessKeyId: credentials.AccessKeyId,
AccessKeySecret: credentials.AccessKeySecret,
DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"),
Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"),
})
return provider, err
})
}
+2
View File
@@ -20,8 +20,10 @@ func init() {
ServerUrl: credentials.ServerUrl,
ApiVersion: credentials.ApiVersion,
ApiRole: credentials.ApiRole,
AuthMethod: credentials.AuthMethod,
Username: credentials.Username,
Password: credentials.Password,
ApiKey: credentials.ApiKey,
AllowInsecureConnections: credentials.AllowInsecureConnections,
DeployTarget: xmaps.GetString(options.ProviderExtendedConfig, "deployTarget"),
CertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, "certificateId"),
@@ -0,0 +1,30 @@
package deployers
import (
"fmt"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core"
dplyimpl "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ddospro"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudDDoSPro, func(options *ProviderFactoryOptions) (core.Deployer, error) {
credentials := domain.AccessConfigForTencentCloud{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{
SecretId: credentials.SecretId,
SecretKey: credentials.SecretKey,
ProjectId: credentials.ProjectId,
Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"),
InstanceId: xmaps.GetString(options.ProviderExtendedConfig, "instanceId"),
DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"),
Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"),
})
return provider, err
})
}
@@ -24,6 +24,7 @@ func init() {
DeployTarget: xmaps.GetString(options.ProviderExtendedConfig, "deployTarget"),
LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"),
ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"),
Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"),
})
return provider, err
})
+9 -2
View File
@@ -81,6 +81,11 @@ type AccessConfigForArvanCloud struct {
ApiKey string `json:"apiKey"`
}
type AccessConfigForAsiaISPCDN struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type AccessConfigForAWS struct {
AuthMethod string `json:"authMethod"`
AccessKeyId string `json:"accessKeyId"`
@@ -379,10 +384,12 @@ type AccessConfigForLarkBot struct {
type AccessConfigForLeCDN struct {
ServerUrl string `json:"serverUrl"`
AuthMethod string `json:"authMethod"`
ApiVersion string `json:"apiVersion"`
ApiRole string `json:"apiRole"`
Username string `json:"username"`
Password string `json:"password"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
ApiKey string `json:"apiKey,omitempty"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
+3
View File
@@ -24,6 +24,7 @@ const (
AccessProviderTypeAliyun = AccessProviderType("aliyun")
AccessProviderTypeAPISIX = AccessProviderType("apisix")
AccessProviderTypeArvanCloud = AccessProviderType("arvancloud")
AccessProviderTypeAsiaISPCDN = AccessProviderType("asiaispcdn")
AccessProviderTypeAWS = AccessProviderType("aws")
AccessProviderTypeAxisNow = AccessProviderType("axisnow")
AccessProviderTypeAzure = AccessProviderType("azure")
@@ -329,6 +330,7 @@ const (
DeploymentProviderTypeAliyunVOD = DeploymentProviderType(AccessProviderTypeAliyun + "-vod")
DeploymentProviderTypeAliyunWAF = DeploymentProviderType(AccessProviderTypeAliyun + "-waf")
DeploymentProviderTypeAPISIX = DeploymentProviderType(AccessProviderTypeAPISIX)
DeploymentProviderTypeAsiaISPCDN = DeploymentProviderType(AccessProviderTypeAsiaISPCDN)
DeploymentProviderTypeAWSACM = DeploymentProviderType(AccessProviderTypeAWS + "-acm")
DeploymentProviderTypeAWSALB = DeploymentProviderType(AccessProviderTypeAWS + "-alb")
DeploymentProviderTypeAWSAmplify = DeploymentProviderType(AccessProviderTypeAWS + "-amplify")
@@ -419,6 +421,7 @@ const (
DeploymentProviderTypeTencentCloudCLB = DeploymentProviderType(AccessProviderTypeTencentCloud + "-clb")
DeploymentProviderTypeTencentCloudCOS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cos")
DeploymentProviderTypeTencentCloudCSS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-css")
DeploymentProviderTypeTencentCloudDDoSPro = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ddospro")
DeploymentProviderTypeTencentCloudECDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ecdn")
DeploymentProviderTypeTencentCloudEO = DeploymentProviderType(AccessProviderTypeTencentCloud + "-eo")
DeploymentProviderTypeTencentCloudEOMakers = DeploymentProviderType(AccessProviderTypeTencentCloud + "-eomakers")
+63
View File
@@ -0,0 +1,63 @@
package migrations
import (
"errors"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
tracer := NewTracer("v0.4.33")
tracer.Printf("go ...")
// update collection `access`
// - modify field `config` schema
{
collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
records, err := app.FindAllRecords(collection)
if err != nil {
return err
}
for _, record := range records {
changed := false
provider := record.GetString("provider")
config := make(map[string]any)
if err := record.UnmarshalJSONField("config", &config); err != nil {
return err
}
switch provider {
case "lecdn":
{
if _, ok := config["authMethod"]; !ok {
config["authMethod"] = "password"
record.Set("config", config)
changed = true
}
}
}
if changed {
if err := app.Save(record); err != nil {
return err
}
tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name)
}
}
}
tracer.Printf("done")
return nil
}, func(app core.App) error {
return errors.ErrUnsupported
})
}
@@ -0,0 +1,50 @@
//go:build tester
package asiaispcdn_test
import (
"testing"
"github.com/stretchr/testify/require"
tester "github.com/certimate-go/certimate/pkg/core/certmgr/providers-tester"
impl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/asiaispcdn"
)
var (
fp = tester.InitArgs("ASIAISPCDN_")
fTestCertPath string
fTestKeyPath string
fAccessKeyId string
fAccessKeySecret string
)
func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fAccessKeyId, "ACCESSKEYID")
fp.DefineString(&fAccessKeySecret, "ACCESSKEYSECRET")
}
/*
Shell command to run this test:
go test -tags=tester -v ./asiaispcdn_test.go -args \
--ASIAISPCDN_TESTCERTPATH="/path/to/your-test-cert.pem" \
--ASIAISPCDN_TESTKEYPATH="/path/to/your-test-key.pem" \
--ASIAISPCDN_ACCESSKEYID="your-access-key-id" \
--ASIAISPCDN_ACCESSKEYSECRET="your-access-key-secret"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Upload", func(t *testing.T) {
provider, err := impl.NewCertmgr(&impl.CertmgrConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
})
require.NoError(t, err)
tester.Upload(t, provider, tester.UploadInput{CertPath: fTestCertPath, KeyPath: fTestKeyPath})
})
}
+30 -22
View File
@@ -286,33 +286,41 @@ const (
)
func createSDKClient(serverUrl, apiVersion, apiKey string, skipTlsVerify bool, nodeName string) (any, error) {
if apiVersion == sdkVersionV1 {
client, err := onepanelsdk.NewClient(serverUrl,
onepanelsdk.WithApiKey(apiKey),
)
if err != nil {
return nil, err
switch apiVersion {
case sdkVersionV1:
{
client, err := onepanelsdk.NewClient(
serverUrl,
onepanelsdk.WithApiKey(apiKey),
)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
case sdkVersionV2:
{
client, err := onepanelsdk2.NewClient(
serverUrl,
onepanelsdk2.WithApiKey(apiKey),
onepanelsdk2.WithNode(nodeName),
)
if err != nil {
return nil, err
}
return client, nil
} else if apiVersion == sdkVersionV2 {
client, err := onepanelsdk2.NewClient(serverUrl,
onepanelsdk2.WithApiKey(apiKey),
onepanelsdk2.WithNode(nodeName),
)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
return client, nil
}
return client, nil
}
return nil, fmt.Errorf("1panel: invalid api version")
@@ -0,0 +1,151 @@
package asiaispcdn
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
asiaispcdnsdk "github.com/certimate-go/certimate/pkg/sdk3rd/asiaispcdn"
)
type (
Provider = core.Certmgr
UploadResult = core.CertmgrUploadResult
ReplaceResult = core.CertmgrReplaceResult
)
type CertmgrConfig struct {
// 橙域网络 CDN AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 橙域网络 CDN AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
}
type Certmgr struct {
config *CertmgrConfig
logger *slog.Logger
sdkClient *asiaispcdnsdk.Client
}
var _ Provider = (*Certmgr)(nil)
func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the certmgr provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
return &Certmgr{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (c *Certmgr) SetLogger(logger *slog.Logger) {
if logger == nil {
c.logger = slog.New(slog.DiscardHandler)
} else {
c.logger = logger
}
}
func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*UploadResult, error) {
// 避免重复上传
if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
} else if upok {
c.logger.Info("ssl certificate already exists")
return upres, nil
}
// 生成新证书名(需符合橙域网络 CDN 命名规则)
certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
// 上传证书
certificateUploadReq := &asiaispcdnsdk.CertificateUploadRequest{
PublicKey: lo.ToPtr(certPEM),
PrivateKey: lo.ToPtr(privkeyPEM),
Name: lo.ToPtr(certName),
}
certificateUploadResp, err := c.sdkClient.CertificateUploadWithContext(ctx, certificateUploadReq)
c.logger.Debug("sdk request 'certificateUpload'", slog.Any("request", certificateUploadReq), slog.Any("response", certificateUploadResp))
if err != nil {
if certificateUploadResp != nil && certificateUploadResp.GetCode() == 80003 {
if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
} else if !upok {
return nil, fmt.Errorf("could not find ssl certificate, may be upload failed")
} else {
c.logger.Info("ssl certificate already exists")
return upres, nil
}
}
return nil, fmt.Errorf("failed to execute sdk request 'certificateUpload': %w", err)
}
certId, err := certificateUploadResp.Data.Int64()
if err != nil {
return nil, fmt.Errorf("received invalid certificate id: %w", err)
}
return &UploadResult{
CertId: fmt.Sprintf("%d", certId),
CertName: certName,
}, nil
}
func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*ReplaceResult, error) {
return nil, core.ErrUnsupported
}
func (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM, privkeyPEM string) (*UploadResult, bool, error) {
// 查询证书列表
certificateQueryListResp, err := c.sdkClient.CertificateQueryListWithContext(ctx)
c.logger.Debug("sdk request 'certificateQueryList'", slog.Any("response", certificateQueryListResp))
if err != nil {
return nil, false, fmt.Errorf("failed to execute sdk request 'certificateQueryList': %w", err)
} else {
for _, certItem := range certificateQueryListResp.Data {
certificateQueryResp, err := c.sdkClient.CertificateQueryWithContext(ctx, certItem.CertId)
c.logger.Debug("sdk request 'certificateQuery'", slog.String("params.certId", certItem.CertId), slog.Any("response", certificateQueryResp))
if err != nil {
if certificateQueryResp != nil && certificateQueryResp.GetCode() == 80012 {
continue
}
return nil, false, fmt.Errorf("failed to execute sdk request 'certificateQuery': %w", err)
}
if certificateQueryResp.Data.PublicKey == certPEM && certificateQueryResp.Data.PrivateKey == privkeyPEM {
// 如果已存在相同证书,直接返回
return &UploadResult{
CertId: certItem.CertId,
CertName: certItem.Name,
}, true, nil
}
}
}
return nil, false, nil
}
func createSDKClient(accessKeyId, accessKeySecret string) (*asiaispcdnsdk.Client, error) {
client, err := asiaispcdnsdk.NewClient(
asiaispcdnsdk.WithAkSk(accessKeyId, accessKeySecret),
)
if err != nil {
return nil, err
}
return client, nil
}
@@ -163,18 +163,11 @@ func createSDKClient(serverUrl, authMethod, username, password, apiToken string,
switch authMethod {
case "", AUTH_METHOD_PASSWORD:
{
client, err = npmsdk.NewClient(serverUrl,
npmsdk.WithLogins(username, password),
)
}
client, err = npmsdk.NewClient(serverUrl, npmsdk.WithLogins(username, password))
case AUTH_METHOD_TOKEN:
{
client, err = npmsdk.NewClient(serverUrl,
npmsdk.WithJwtToken(apiToken),
)
}
client, err = npmsdk.NewClient(serverUrl, npmsdk.WithJwtToken(apiToken))
default:
err = fmt.Errorf("unsupported auth method '%s'", authMethod)
}
if err != nil {
@@ -0,0 +1,55 @@
//go:build tester
package asiaispcdn_test
import (
"testing"
"github.com/stretchr/testify/require"
tester "github.com/certimate-go/certimate/pkg/core/deployer/providers-tester"
impl "github.com/certimate-go/certimate/pkg/core/deployer/providers/asiaispcdn"
)
var (
fp = tester.InitArgs("ASIAISPCDN_")
fTestCertPath string
fTestKeyPath string
fAccessKeyId string
fAccessKeySecret string
fDomain string
)
func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fAccessKeyId, "ACCESSKEYID")
fp.DefineString(&fAccessKeySecret, "ACCESSKEYSECRET")
fp.DefineString(&fDomain, "DOMAIN")
}
/*
Shell command to run this test:
go test -tags=tester -v ./asiaispcdn_test.go -args \
--ASIAISPCDN_TESTCERTPATH="/path/to/your-test-cert.pem" \
--ASIAISPCDN_TESTKEYPATH="/path/to/your-test-key.pem" \
--ASIAISPCDN_ACCESSKEYID="your-access-key-id" \
--ASIAISPCDN_ACCESSKEYSECRET="your-access-key-secret" \
--ASIAISPCDN_DOMAIN="example.com"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Deploy", func(t *testing.T) {
provider, err := impl.NewDeployer(&impl.DeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
DomainMatchPattern: impl.DOMAIN_MATCH_PATTERN_EXACT,
Domain: fDomain,
})
require.NoError(t, err)
tester.Deploy(t, provider, tester.DeployInput{CertPath: fTestCertPath, KeyPath: fTestKeyPath})
})
}
@@ -26,7 +26,7 @@ func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fServerUrl, "SERVERURL")
fp.DefineString(&fApiVersion, "APIVERSION", "v3")
fp.DefineString(&fApiVersion, "APIVERSION", "v4")
fp.DefineString(&fUsername, "USERNAME")
fp.DefineString(&fPassword, "PASSWORD")
fp.DefineInt64(&fCertificateId, "CERTIFICATEID")
@@ -0,0 +1,59 @@
//go:build tester
package tencentcloudddospro_test
import (
"testing"
"github.com/stretchr/testify/require"
tester "github.com/certimate-go/certimate/pkg/core/deployer/providers-tester"
impl "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ddospro"
)
var (
fp = tester.InitArgs("TENCENTCLOUDDDOSPRO_")
fTestCertPath string
fTestKeyPath string
fSecretId string
fSecretKey string
fInstanceId string
fDomain string
)
func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fSecretId, "SECRETID")
fp.DefineString(&fSecretKey, "SECRETKEY")
fp.DefineString(&fInstanceId, "INSTANCEID")
fp.DefineString(&fDomain, "DOMAIN")
}
/*
Shell command to run this test:
go test -tags=tester -v ./tencentcloud_ddospro_test.go -args \
--TENCENTCLOUDDDOSPRO_TESTCERTPATH="/path/to/your-test-cert.pem" \
--TENCENTCLOUDDDOSPRO_TESTKEYPATH="/path/to/your-test-key.pem" \
--TENCENTCLOUDDDOSPRO_SECRETID="your-secret-id" \
--TENCENTCLOUDDDOSPRO_SECRETKEY="your-secret-key" \
--TENCENTCLOUDDDOSPRO_INSTANCEID="your-instance-id" \
--TENCENTCLOUDDDOSPRO_DOMAIN="example.com"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Deploy", func(t *testing.T) {
provider, err := impl.NewDeployer(&impl.DeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
InstanceId: fInstanceId,
DomainMatchPattern: impl.DOMAIN_MATCH_PATTERN_EXACT,
Domain: fDomain,
})
require.NoError(t, err)
tester.Deploy(t, provider, tester.DeployInput{CertPath: fTestCertPath, KeyPath: fTestKeyPath})
})
}
@@ -19,6 +19,7 @@ var (
fSecretAccessKey string
fRegion string
fListenerId string
fDomain string
)
func init() {
@@ -28,6 +29,7 @@ func init() {
fp.DefineString(&fSecretAccessKey, "SECRETACCESSKEY")
fp.DefineString(&fRegion, "REGION")
fp.DefineString(&fListenerId, "LISTENERID")
fp.DefineString(&fDomain, "DOMAIN")
}
/*
@@ -39,7 +41,8 @@ Shell command to run this test:
--VOLCENGINECLB_ACCESSKEYID="your-access-key-id" \
--VOLCENGINECLB_SECRETACCESSKEY="your-secret-access-key" \
--VOLCENGINECLB_REGION="cn-beijing" \
--VOLCENGINECLB_LISTENERID="your-listener-id"
--VOLCENGINECLB_LISTENERID="your-listener-id" \
--VOLCENGINECLB_DOMAIN="example.com"
*/
func TestProvider(t *testing.T) {
fp.Parse()
@@ -51,6 +54,7 @@ func TestProvider(t *testing.T) {
Region: fRegion,
DeployTarget: impl.DEPLOY_TARGET_LISTENER,
ListenerId: fListenerId,
Domain: fDomain,
})
require.NoError(t, err)
@@ -112,32 +112,40 @@ const (
)
func createSDKClient(serverUrl, apiVersion, apiKey string, skipTlsVerify bool) (any, error) {
if apiVersion == sdkVersionV1 {
client, err := onepanelsdk.NewClient(serverUrl,
onepanelsdk.WithApiKey(apiKey),
)
if err != nil {
return nil, err
switch apiVersion {
case sdkVersionV1:
{
client, err := onepanelsdk.NewClient(
serverUrl,
onepanelsdk.WithApiKey(apiKey),
)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
case sdkVersionV2:
{
client, err := onepanelsdk2.NewClient(
serverUrl,
onepanelsdk2.WithApiKey(apiKey),
)
if err != nil {
return nil, err
}
return client, nil
} else if apiVersion == sdkVersionV2 {
client, err := onepanelsdk2.NewClient(serverUrl,
onepanelsdk2.WithApiKey(apiKey),
)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
return client, nil
}
return client, nil
}
return nil, fmt.Errorf("1panel: invalid api version")
+30 -22
View File
@@ -391,33 +391,41 @@ const (
)
func createSDKClient(serverUrl, apiVersion, apiKey string, skipTlsVerify bool, nodeName string) (any, error) {
if apiVersion == sdkVersionV1 {
client, err := onepanelsdk.NewClient(serverUrl,
onepanelsdk.WithApiKey(apiKey),
)
if err != nil {
return nil, err
switch apiVersion {
case sdkVersionV1:
{
client, err := onepanelsdk.NewClient(
serverUrl,
onepanelsdk.WithApiKey(apiKey),
)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
case sdkVersionV2:
{
client, err := onepanelsdk2.NewClient(
serverUrl,
onepanelsdk2.WithApiKey(apiKey),
onepanelsdk2.WithNode(nodeName),
)
if err != nil {
return nil, err
}
return client, nil
} else if apiVersion == sdkVersionV2 {
client, err := onepanelsdk2.NewClient(serverUrl,
onepanelsdk2.WithApiKey(apiKey),
onepanelsdk2.WithNode(nodeName),
)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
return client, nil
}
return client, nil
}
return nil, fmt.Errorf("1panel: invalid api version")
@@ -225,6 +225,7 @@ func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudLoadbalan
d.logger.Info("no need to deploy clb listener default certificate")
return nil
}
return d.updateListenerDefaultCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)
} else {
// 指定 SNI,需部署到扩展域名
@@ -240,6 +240,7 @@ func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudAccelerat
d.logger.Info("no need to deploy ga listener default certificate")
return nil
}
return d.updateListenerDefaultCertificate(ctx, cloudListenerId, cloudCertId)
} else {
// 指定 SNI,需部署到扩展域名
@@ -0,0 +1,271 @@
package asiaispcdn
import (
"context"
"fmt"
"log/slog"
"strconv"
"strings"
"time"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/asiaispcdn"
asiaispcdnsdk "github.com/certimate-go/certimate/pkg/sdk3rd/asiaispcdn"
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
xloop "github.com/certimate-go/certimate/pkg/utils/loop"
xwait "github.com/certimate-go/certimate/pkg/utils/wait"
)
type (
Provider = core.Deployer
DeployResult = core.DeployerDeployResult
)
type DeployerConfig struct {
// 橙域网络 CDN AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 橙域网络 CDN AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 域名匹配模式。
// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。
DomainMatchPattern string `json:"domainMatchPattern,omitempty"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *asiaispcdnsdk.Client
sdkCertmgr core.Certmgr
}
var _ Provider = (*Deployer)(nil)
func NewDeployer(config *DeployerConfig) (*Deployer, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{
AccessKeyId: config.AccessKeyId,
AccessKeySecret: config.AccessKeySecret,
})
if err != nil {
return nil, fmt.Errorf("could not create certmgr: %w", err)
}
return &Deployer{
config: config,
logger: slog.Default(),
sdkClient: client,
sdkCertmgr: pcertmgr,
}, nil
}
func (d *Deployer) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sdkCertmgr.SetLogger(logger)
}
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
// 上传证书
upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 获取待部署的域名列表
var domains []string
switch d.config.DomainMatchPattern {
case "", DOMAIN_MATCH_PATTERN_EXACT:
{
if d.config.Domain == "" {
return nil, fmt.Errorf("config `domain` is required")
}
domains = []string{d.config.Domain}
}
case DOMAIN_MATCH_PATTERN_WILDCARD:
{
if d.config.Domain == "" {
return nil, fmt.Errorf("config `domain` is required")
}
if strings.HasPrefix(d.config.Domain, "*.") {
domainCandidates, err := d.getAllDomains(ctx)
if err != nil {
return nil, err
}
domains = lo.Filter(domainCandidates, func(domain string, _ int) bool {
return xcerthostname.IsMatch(d.config.Domain, domain)
})
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by wildcard")
}
} else {
domains = []string{d.config.Domain}
}
}
case DOMAIN_MATCH_PATTERN_CERTSAN:
{
domainCandidates, err := d.getAllDomains(ctx)
if err != nil {
return nil, err
}
domains = lo.Filter(domainCandidates, func(domain string, _ int) bool {
return xcerthostname.IsMatchByCertificatePEM(certPEM, domain)
})
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by certificate")
}
}
default:
return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern)
}
// 批量更新域名证书
if len(domains) == 0 {
d.logger.Info("no cdn domains to deploy")
} else {
d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains))
if err := xloop.ForRangeAllWithContext(ctx, domains, func(ctx context.Context, domain string, _ int) error {
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
return d.updateDomainCertificate(ctx, domain, certId)
}); err != nil {
return nil, err
}
}
return &DeployResult{}, nil
}
func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {
domains := make([]string, 0)
// 查询域名信息列表
domainQueryListReq := &asiaispcdnsdk.DomainQueryListRequest{}
domainQueryListResp, err := d.sdkClient.DomainQueryListWithContext(ctx, domainQueryListReq)
d.logger.Debug("sdk request 'domainQueryList'", slog.Any("request", domainQueryListReq), slog.Any("response", domainQueryListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'domainQueryList': %w", err)
}
for _, domainItem := range domainQueryListResp.Data {
if domainItem.DomainStatus == 0 {
continue
}
domains = append(domains, domainItem.Domain)
}
return domains, nil
}
func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId int64) error {
// 查询域名配置
domainQueryListReq := &asiaispcdnsdk.DomainQueryListRequest{
Domain: lo.ToPtr(domain),
}
domainQueryListResp, err := d.sdkClient.DomainQueryListWithContext(ctx, domainQueryListReq)
d.logger.Debug("sdk request 'domainQueryList'", slog.Any("request", domainQueryListReq), slog.Any("response", domainQueryListResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'domainQueryList': %w", err)
} else {
domainEntry, _ := lo.Find(domainQueryListResp.Data, func(domainInfo *asiaispcdnsdk.Domain) bool {
return domainInfo.Domain == domain
})
if domainEntry == nil {
return fmt.Errorf("could not find domain '%s'", domain)
}
if domainEntry.CertId == cloudCertId {
// 已部署过,直接返回
return nil
}
if domainEntry.DomainStatus == 2 {
if err := d.waitForDomainReady(ctx, domain); err != nil {
return err
}
}
}
// 修改域名配置
domainModifyReq := &asiaispcdnsdk.DomainModifyRequest{
Domain: lo.ToPtr(domain),
Protocol: lo.ToPtr("https"),
CertId: lo.ToPtr(cloudCertId),
}
domainModifyResp, err := d.sdkClient.DomainModifyWithContext(ctx, domainModifyReq)
d.logger.Debug("sdk request 'domainModify'", slog.Any("request", domainModifyReq), slog.Any("response", domainModifyResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'domainModify': %w", err)
}
return nil
}
func (d *Deployer) waitForDomainReady(ctx context.Context, domain string) error {
// 查询域名信息,直到操作状态不再为 "2"
if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) {
domainQueryListReq := &asiaispcdnsdk.DomainQueryListRequest{
Domain: lo.ToPtr(domain),
}
domainQueryListResp, err := d.sdkClient.DomainQueryListWithContext(ctx, domainQueryListReq)
d.logger.Debug("sdk request 'domainQueryList'", slog.Any("request", domainQueryListReq), slog.Any("response", domainQueryListResp))
if err != nil {
return false, fmt.Errorf("failed to execute sdk request 'domainQueryList': %w", err)
}
domainEntry, _ := lo.Find(domainQueryListResp.Data, func(domainInfo *asiaispcdnsdk.Domain) bool {
return domainInfo.Domain == domain
})
if domainEntry == nil {
return false, fmt.Errorf("could not find domain '%s'", domain)
}
if domainEntry.DomainStatus != 2 {
return true, nil
}
d.logger.Info("waiting for domain's status to not be 'Operating' ...")
return false, nil
}, 10*time.Second); err != nil {
return err
}
return nil
}
func createSDKClient(accessKeyId, accessKeySecret string) (*asiaispcdnsdk.Client, error) {
client, err := asiaispcdnsdk.NewClient(
asiaispcdnsdk.WithAkSk(accessKeyId, accessKeySecret),
)
if err != nil {
return nil, err
}
return client, nil
}
@@ -0,0 +1,10 @@
package asiaispcdn
const (
// 匹配模式:精确匹配。
DOMAIN_MATCH_PATTERN_EXACT = "exact"
// 匹配模式:通配符匹配。
DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard"
// 匹配模式:证书 SAN 匹配。
DOMAIN_MATCH_PATTERN_CERTSAN = "certsan"
)
@@ -240,6 +240,7 @@ func (d *Deployer) updateHttpsListenerCertificate(ctx context.Context, cloudLoad
d.logger.Info("no need to deploy appblb listener default certificate")
return nil
}
return d.updateHttpsListenerDefaultCertificate(ctx, cloudLoadbalancerId, &listenerInfo, cloudCertId)
} else {
// 指定 SNI,需部署到扩展域名
@@ -240,6 +240,7 @@ func (d *Deployer) updateHttpsListenerCertificate(ctx context.Context, cloudLoad
d.logger.Info("no need to deploy blb listener default certificate")
return nil
}
return d.updateHttpsListenerDefaultCertificate(ctx, cloudLoadbalancerId, &listenerInfo, cloudCertId)
} else {
// 指定 SNI,需部署到扩展域名
@@ -213,6 +213,7 @@ func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerI
d.logger.Info("no need to deploy alb listener default certificate")
return nil
}
return d.updateListenerDefaultCertificate(ctx, *describeListenerAttributesResp, cloudCertId)
} else {
// 指定 SNI,需部署到扩展域名
@@ -252,7 +253,8 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListen
CertCenterCertificateId: bp.String(cloudCertId),
Action: bp.String("modify"),
}
}),
},
),
}
modifyListenerAttributesResp, err := d.sdkClient.ModifyListenerAttributesWithContext(ctx, modifyListenerAttributesReq)
d.logger.Debug("sdk request 'alb.ModifyListenerAttributes'", slog.Any("request", modifyListenerAttributesReq), slog.Any("response", modifyListenerAttributesResp))
@@ -112,6 +112,9 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
}
domains = domainCandidates
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by wildcard")
}
} else {
domains = []string{d.config.Domain}
}
@@ -125,6 +128,9 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
}
domains = domainCandidates
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by certificate")
}
}
default:
@@ -216,12 +222,6 @@ func (d *Deployer) getMatchedDomainsByCertId(ctx context.Context, cloudCertId st
}
}
if len(domains) == 0 {
if len(describeCertConfigResp.Result.SpecifiedCertConfig) == 0 {
return nil, fmt.Errorf("could not find any domains matched by certificate")
}
}
return domains, nil
}
@@ -1,5 +1,10 @@
package lecdn
const (
AUTH_METHOD_PASSWORD = "password"
AUTH_METHOD_APIKEY = "apikey"
)
const (
// 部署目标:替换指定证书。
DEPLOY_TARGET_CERTIFICATE = "certificate"
+169 -38
View File
@@ -8,8 +8,10 @@ import (
"time"
"github.com/certimate-go/certimate/pkg/core"
lecdnclientv3 "github.com/certimate-go/certimate/pkg/sdk3rd/lecdn/v3/client"
lecdnmasterv3 "github.com/certimate-go/certimate/pkg/sdk3rd/lecdn/v3/master"
lecdnclientsdkv3 "github.com/certimate-go/certimate/pkg/sdk3rd/lecdn/client/v3"
lecdnclientsdkv4 "github.com/certimate-go/certimate/pkg/sdk3rd/lecdn/client/v4"
lecdnmastersdkv3 "github.com/certimate-go/certimate/pkg/sdk3rd/lecdn/master/v3"
lecdnmastersdkv4 "github.com/certimate-go/certimate/pkg/sdk3rd/lecdn/master/v4"
)
type (
@@ -21,15 +23,21 @@ type DeployerConfig struct {
// LeCDN 服务地址。
ServerUrl string `json:"serverUrl"`
// LeCDN 版本。
// 可取值 "v3"。
// 可取值 "v3"、"v4"。
ApiVersion string `json:"apiVersion"`
// LeCDN 用户角色。
// 可取值 "client"、"master"。
ApiRole string `json:"apiRole"`
// LeCDN API 认证方式。
// 可取值 "password"、"apikey"。
// 零值时默认值 [AUTH_METHOD_PASSWORD]。
AuthMethod string `json:"authMethod,omitempty"`
// LeCDN 用户名。
Username string `json:"username"`
Username string `json:"username,omitempty"`
// LeCDN 用户密码。
Password string `json:"password"`
Password string `json:"password,omitempty"`
// LeCDN API Key。
ApiKey string `json:"apiKey,omitempty"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 部署目标。
@@ -55,7 +63,7 @@ func NewDeployer(config *DeployerConfig) (*Deployer, error) {
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiRole, config.Username, config.Password, config.AllowInsecureConnections)
client, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiRole, config.AuthMethod, config.Username, config.Password, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
@@ -95,14 +103,18 @@ func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM
return fmt.Errorf("config `certificateId` is required")
}
// 生成新证书名(需符合 LeCDN 命名规则)
certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
certDesc := "upload from Certimate"
// 修改证书
// REF: https://wdk0pwf8ul.feishu.cn/wiki/YE1XwCRIHiLYeKkPupgcXrlgnDd
switch sdkClient := d.sdkClient.(type) {
case *lecdnclientv3.Client:
case *lecdnclientsdkv3.Client:
{
updateSSLCertReq := &lecdnclientv3.UpdateCertificateRequest{
Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
Description: "upload from Certimate",
updateSSLCertReq := &lecdnclientsdkv3.UpdateCertificateRequest{
Name: certName,
Description: certDesc,
Type: "upload",
SSLPEM: certPEM,
SSLKey: privkeyPEM,
@@ -115,12 +127,12 @@ func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM
}
}
case *lecdnmasterv3.Client:
case *lecdnmastersdkv3.Client:
{
updateSSLCertReq := &lecdnmasterv3.UpdateCertificateRequest{
updateSSLCertReq := &lecdnmastersdkv3.UpdateCertificateRequest{
ClientId: d.config.ClientId,
Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
Description: "upload from Certimate",
Name: certName,
Description: certDesc,
Type: "upload",
SSLPEM: certPEM,
SSLKey: privkeyPEM,
@@ -133,6 +145,38 @@ func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM
}
}
case *lecdnclientsdkv4.Client:
{
updateSSLCertReq := &lecdnclientsdkv4.UpdateCertificateRequest{
Name: certName,
Description: certDesc,
SSLPEM: certPEM,
SSLKey: privkeyPEM,
AutoRenewal: false,
}
updateSSLCertResp, err := sdkClient.UpdateCertificateWithContext(ctx, d.config.CertificateId, updateSSLCertReq)
d.logger.Debug("sdk request 'UpdateCertificate'", slog.Int64("params.certId", d.config.CertificateId), slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'UpdateCertificate': %w", err)
}
}
case *lecdnmastersdkv4.Client:
{
updateSSLCertReq := &lecdnmastersdkv4.UpdateCertificateRequest{
Name: certName,
Description: certDesc,
SSLPEM: certPEM,
SSLKey: privkeyPEM,
AutoRenewal: false,
}
updateSSLCertResp, err := sdkClient.UpdateCertificateWithContext(ctx, d.config.CertificateId, updateSSLCertReq)
d.logger.Debug("sdk request 'UpdateCertificate'", slog.Int64("params.certId", d.config.CertificateId), slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'UpdateCertificate': %w", err)
}
}
default:
panic("unreachable")
}
@@ -142,40 +186,127 @@ func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM
const (
sdkVersionV3 = "v3"
sdkVersionV4 = "v4"
sdkRoleClient = "client"
sdkRoleMaster = "master"
)
func createSDKClient(serverUrl, apiVersion, apiRole, username, password string, skipTlsVerify bool) (any, error) {
if apiVersion == sdkVersionV3 && apiRole == sdkRoleClient {
// v3 版客户端
client, err := lecdnclientv3.NewClient(serverUrl,
lecdnclientv3.WithLogins(username, password),
)
if err != nil {
return nil, err
func createSDKClient(serverUrl, apiVersion, apiRole, authMethod, username, password, apiKey string, skipTlsVerify bool) (any, error) {
switch apiVersion {
case sdkVersionV3:
{
switch apiRole {
case sdkRoleClient:
{
// v3 版客户端
var client *lecdnclientsdkv3.Client
var err error
switch authMethod {
case "", AUTH_METHOD_PASSWORD:
client, err = lecdnclientsdkv3.NewClient(serverUrl, lecdnclientsdkv3.WithLogins(username, password))
case AUTH_METHOD_APIKEY:
client, err = lecdnclientsdkv3.NewClient(serverUrl, lecdnclientsdkv3.WithLogins(username, password))
default:
err = fmt.Errorf("unsupported auth method '%s'", authMethod)
}
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}
case sdkRoleMaster:
{
// v3 版主控端
var client *lecdnmastersdkv3.Client
var err error
switch authMethod {
case "", AUTH_METHOD_PASSWORD:
client, err = lecdnmastersdkv3.NewClient(serverUrl, lecdnmastersdkv3.WithLogins(username, password))
case AUTH_METHOD_APIKEY:
client, err = lecdnmastersdkv3.NewClient(serverUrl, lecdnmastersdkv3.WithLogins(username, password))
default:
err = fmt.Errorf("unsupported auth method '%s'", authMethod)
}
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}
}
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
case sdkVersionV4:
{
switch apiRole {
case sdkRoleClient:
{
// v4 版客户端
var client *lecdnclientsdkv4.Client
var err error
return client, nil
} else if apiVersion == sdkVersionV3 && apiRole == sdkRoleMaster {
// v3 版主控端
client, err := lecdnmasterv3.NewClient(serverUrl,
lecdnmasterv3.WithLogins(username, password),
)
if err != nil {
return nil, err
}
switch authMethod {
case "", AUTH_METHOD_PASSWORD:
client, err = lecdnclientsdkv4.NewClient(serverUrl, lecdnclientsdkv4.WithLogins(username, password))
case AUTH_METHOD_APIKEY:
client, err = lecdnclientsdkv4.NewClient(serverUrl, lecdnclientsdkv4.WithLogins(username, password))
default:
err = fmt.Errorf("unsupported auth method '%s'", authMethod)
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
if err != nil {
return nil, err
}
return client, nil
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}
case sdkRoleMaster:
{
// v4 版主控端
var client *lecdnmastersdkv4.Client
var err error
switch authMethod {
case "", AUTH_METHOD_PASSWORD:
client, err = lecdnmastersdkv4.NewClient(serverUrl, lecdnmastersdkv4.WithLogins(username, password))
case AUTH_METHOD_APIKEY:
client, err = lecdnmastersdkv4.NewClient(serverUrl, lecdnmastersdkv4.WithLogins(username, password))
default:
err = fmt.Errorf("unsupported auth method '%s'", authMethod)
}
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}
}
}
}
return nil, fmt.Errorf("lecdn: invalid api version or user role")
@@ -382,20 +382,11 @@ func createSDKClient(serverUrl, authMethod, username, password, apiToken string,
switch authMethod {
case "", AUTH_METHOD_PASSWORD:
{
client, err = npmsdk.NewClient(
serverUrl,
npmsdk.WithLogins(username, password),
)
}
client, err = npmsdk.NewClient(serverUrl, npmsdk.WithLogins(username, password))
case AUTH_METHOD_TOKEN:
{
client, err = npmsdk.NewClient(
serverUrl,
npmsdk.WithJwtToken(apiToken),
)
}
client, err = npmsdk.NewClient(serverUrl, npmsdk.WithJwtToken(apiToken))
default:
err = fmt.Errorf("unsupported auth method '%s'", authMethod)
}
if err != nil {
@@ -0,0 +1,10 @@
package tencentcloudddospro
const (
// 匹配模式:精确匹配。
DOMAIN_MATCH_PATTERN_EXACT = "exact"
// 匹配模式:通配符匹配。
DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard"
// 匹配模式:证书 SAN 匹配。
DOMAIN_MATCH_PATTERN_CERTSAN = "certsan"
)
@@ -0,0 +1,289 @@
package tencentcloudddospro
import (
"context"
"fmt"
"log/slog"
"strings"
"github.com/samber/lo"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tcantiddos "github.com/certimate-go/certimate/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/antiddos/v20200309"
"github.com/certimate-go/certimate/pkg/core"
cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl"
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
xloop "github.com/certimate-go/certimate/pkg/utils/loop"
xtencentcloud "github.com/certimate-go/certimate/pkg/utils/third-party/tencentcloud"
)
type (
Provider = core.Deployer
DeployResult = core.DeployerDeployResult
)
type DeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 腾讯云项目 ID。
ProjectId int64 `json:"projectId,omitempty"`
// 腾讯云接口端点。
Endpoint string `json:"endpoint,omitempty"`
// DDoS 高防实例 ID。
InstanceId string `json:"instanceId"`
// 域名匹配模式。
// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。
DomainMatchPattern string `json:"domainMatchPattern,omitempty"`
// 网站域名(支持泛域名)。
Domain string `json:"domain"`
}
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *tcantiddos.Client
sdkCertmgr core.Certmgr
}
var _ Provider = (*Deployer)(nil)
func NewDeployer(config *DeployerConfig) (*Deployer, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
}
client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
ProjectId: config.ProjectId,
Endpoint: lo.Ternary(xtencentcloud.IsIntlAPIEndpoint(config.Endpoint), "ssl.intl.tencentcloudapi.com", ""),
})
if err != nil {
return nil, fmt.Errorf("could not create certmgr: %w", err)
}
return &Deployer{
config: config,
logger: slog.Default(),
sdkClient: client,
sdkCertmgr: pcertmgr,
}, nil
}
func (d *Deployer) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sdkCertmgr.SetLogger(logger)
}
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
if d.config.InstanceId == "" {
return nil, fmt.Errorf("config `instanceId` is required")
}
if d.config.Domain == "" {
return nil, fmt.Errorf("config `domain` is required")
}
// 上传证书
upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 获取待部署的域名列表
domains := make([]string, 0)
switch d.config.DomainMatchPattern {
case "", DOMAIN_MATCH_PATTERN_EXACT:
{
if d.config.Domain == "" {
return nil, fmt.Errorf("config `domain` is required")
}
domains = []string{d.config.Domain}
}
case DOMAIN_MATCH_PATTERN_WILDCARD:
{
if d.config.Domain == "" {
return nil, fmt.Errorf("config `domain` is required")
}
if strings.HasPrefix(d.config.Domain, "*.") {
domainCandidates, err := d.getMatchedDomainsByCertId(ctx, upres.CertId)
if err != nil {
return nil, err
}
domains = lo.Filter(domainCandidates, func(domain string, _ int) bool {
return xcerthostname.IsMatch(d.config.Domain, domain)
})
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by wildcard")
}
} else {
domains = []string{d.config.Domain}
}
}
case DOMAIN_MATCH_PATTERN_CERTSAN:
{
domainCandidates, err := d.getMatchedDomainsByCertId(ctx, upres.CertId)
if err != nil {
return nil, err
}
domains = domainCandidates
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by certificate")
}
}
default:
return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern)
}
// 批量绑定证书
if len(domains) == 0 {
d.logger.Info("no anti-ddos domains to deploy")
} else {
d.logger.Info("found anti-ddos domains to deploy", slog.Any("domains", domains))
if err := xloop.ForRangeAllWithContext(ctx, domains, func(ctx context.Context, domain string, _ int) error {
return d.updateDomainCertificate(ctx, domain, upres.CertId)
}); err != nil {
return nil, err
}
}
return &DeployResult{}, nil
}
func (d *Deployer) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) {
domains := make([]string, 0)
// 获取指定证书可关联的域名
// REF: https://cloud.tencent.com/document/api/297/95348
describeL7RulesBySSLCertIdReq := tcantiddos.NewDescribeL7RulesBySSLCertIdRequest()
describeL7RulesBySSLCertIdReq.Status = common.StringPtr("all")
describeL7RulesBySSLCertIdReq.CertIds = []*string{common.StringPtr(cloudCertId)}
describeL7RulesBySSLCertIdResp, err := d.sdkClient.DescribeL7RulesBySSLCertIdWithContext(ctx, describeL7RulesBySSLCertIdReq)
d.logger.Debug("sdk request 'antiddos.DescribeL7RulesBySSLCertId'", slog.Any("request", describeL7RulesBySSLCertIdReq), slog.Any("response", describeL7RulesBySSLCertIdResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'antiddos.DescribeL7RulesBySSLCertId': %w", err)
} else if len(describeL7RulesBySSLCertIdResp.Response.CertSet) == 0 {
return nil, fmt.Errorf("could not find any domains matched by certificate")
}
certEntry := describeL7RulesBySSLCertIdResp.Response.CertSet[0]
for i := range certEntry.L7Rules {
if lo.FromPtr(certEntry.L7Rules[i].InsId) == d.config.InstanceId {
domains = append(domains, lo.FromPtr(certEntry.L7Rules[i].Domain))
}
}
return domains, nil
}
func (d *Deployer) updateDomainCertificate(ctx context.Context, domain, cloudCertId string) error {
// 高防 IP 获取 7 层规则
// REF: https://cloud.tencent.com/document/api/297/95344
var ruleEntry *tcantiddos.NewL7RuleEntry
describeNewL7RulesOffset := 0
describeNewL7RulesLimit := 100
for {
describeNewL7RulesReq := tcantiddos.NewDescribeNewL7RulesRequest()
describeNewL7RulesReq.Business = common.StringPtr("bgpip")
describeNewL7RulesReq.Domain = common.StringPtr(domain)
describeNewL7RulesReq.Offset = common.Uint64Ptr(uint64(describeNewL7RulesOffset))
describeNewL7RulesReq.Limit = common.Uint64Ptr(uint64(describeNewL7RulesLimit))
describeNewL7RulesResp, err := d.sdkClient.DescribeNewL7RulesWithContext(ctx, describeNewL7RulesReq)
d.logger.Debug("sdk request 'antiddos.DescribeNewL7Rules'", slog.Any("request", describeNewL7RulesReq), slog.Any("response", describeNewL7RulesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'antiddos.DescribeNewL7Rules': %w", err)
}
for _, ruleItem := range describeNewL7RulesResp.Response.Rules {
if lo.FromPtr(ruleItem.Domain) == domain {
ruleEntry = ruleItem
break
}
}
if len(describeNewL7RulesResp.Response.Rules) < describeNewL7RulesLimit {
break
}
describeNewL7RulesOffset += describeNewL7RulesLimit
}
if ruleEntry == nil {
return fmt.Errorf("could not find domain '%s'", domain)
} else if lo.FromPtr(ruleEntry.CertType) == 2 && lo.FromPtr(ruleEntry.SSLId) == cloudCertId {
return nil
}
// 修改 7 层转发规则
modifyNewDomainRulesReq := tcantiddos.NewModifyNewDomainRulesRequest()
modifyNewDomainRulesReq.Business = common.StringPtr("bgpip")
modifyNewDomainRulesReq.Id = common.StringPtr(d.config.InstanceId)
modifyNewDomainRulesReq.Rule = &tcantiddos.NewL7RuleEntry{
Protocol: ruleEntry.Protocol,
LbType: ruleEntry.LbType,
Domain: ruleEntry.Domain,
KeepEnable: ruleEntry.KeepEnable,
KeepTime: ruleEntry.KeepTime,
SourceType: ruleEntry.SourceType,
SourceList: ruleEntry.SourceList,
Region: ruleEntry.Region,
Id: ruleEntry.Id,
Ip: ruleEntry.Ip,
RuleId: ruleEntry.RuleId,
RuleName: ruleEntry.RuleName,
CertType: common.Uint64Ptr(2),
SSLId: common.StringPtr(cloudCertId),
CCStatus: ruleEntry.CCStatus,
CCEnable: ruleEntry.CCEnable,
CCThresholdNew: ruleEntry.CCThresholdNew,
CCLevel: ruleEntry.CCLevel,
HttpsToHttpEnable: ruleEntry.HttpsToHttpEnable,
VirtualPort: ruleEntry.VirtualPort,
RewriteHttps: ruleEntry.RewriteHttps,
}
modifyNewDomainRulesResp, err := d.sdkClient.ModifyNewDomainRulesWithContext(ctx, modifyNewDomainRulesReq)
d.logger.Debug("sdk request 'antiddos.ModifyNewDomainRules'", slog.Any("request", modifyNewDomainRulesReq), slog.Any("response", modifyNewDomainRulesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'antiddos.ModifyNewDomainRules': %w", err)
}
return nil
}
func createSDKClient(secretId, secretKey, endpoint string) (*tcantiddos.Client, error) {
credential := common.NewCredential(secretId, secretKey)
cpf := profile.NewClientProfile()
if endpoint != "" {
cpf.HttpProfile.Endpoint = endpoint
}
client, err := tcantiddos.NewClient(credential, "", cpf)
if err != nil {
return nil, err
}
return client, nil
}
@@ -216,8 +216,16 @@ func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerI
d.logger.Info("no need to deploy alb listener default certificate")
return nil
}
return d.updateListenerDefaultCertificate(ctx, *describeListenerAttributesResp, cloudCertId)
} else {
if lo.SomeBy(describeListenerAttributesResp.DomainExtensions, func(domainExtension *vealb.DomainExtensionForDescribeListenerAttributesOutput) bool {
return ve.StringValue(domainExtension.Domain) == d.config.Domain && ve.StringValue(domainExtension.CertCenterCertificateId) == cloudCertId
}) {
d.logger.Info("no need to deploy alb listener sni certificate")
return nil
}
// 指定 SNI,需部署到扩展域名
return d.updateListenerSniCertificate(ctx, *describeListenerAttributesResp, cloudCertId)
}
@@ -247,7 +255,7 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListen
ListenerId: cloudListenerInfo.ListenerId,
DomainExtensions: lo.Map(
lo.Filter(cloudListenerInfo.DomainExtensions, func(domain *vealb.DomainExtensionForDescribeListenerAttributesOutput, _ int) bool {
return *domain.Domain == d.config.Domain
return ve.StringValue(domain.Domain) == d.config.Domain
}),
func(domain *vealb.DomainExtensionForDescribeListenerAttributesOutput, _ int) *vealb.DomainExtensionForModifyListenerAttributesInput {
return &vealb.DomainExtensionForModifyListenerAttributesInput{
@@ -257,7 +265,8 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListen
CertCenterCertificateId: ve.String(cloudCertId),
Action: ve.String("modify"),
}
}),
},
),
}
modifyListenerAttributesResp, err := d.sdkClient.ModifyListenerAttributesWithContext(ctx, modifyListenerAttributesReq)
d.logger.Debug("sdk request 'alb.ModifyListenerAttributes'", slog.Any("request", modifyListenerAttributesReq), slog.Any("response", modifyListenerAttributesResp))
@@ -117,6 +117,9 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
}
domains = domainCandidates
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by wildcard")
}
} else {
domains = []string{d.config.Domain}
}
@@ -130,6 +133,9 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
}
domains = domainCandidates
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by certificate")
}
}
default:
@@ -192,10 +198,6 @@ func (d *Deployer) getMatchedDomainsByWildcard(ctx context.Context, wildcardDoma
listCdnDomainsPageSize++
}
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by wildcard")
}
return domains, nil
}
@@ -225,12 +227,6 @@ func (d *Deployer) getMatchedDomainsByCertId(ctx context.Context, cloudCertId st
}
}
if len(domains) == 0 {
if len(describeCertConfigResp.SpecifiedCertConfig) == 0 {
return nil, fmt.Errorf("could not find any domains matched by certificate")
}
}
return domains, nil
}
@@ -5,6 +5,7 @@ import (
"fmt"
"log/slog"
"github.com/samber/lo"
ve "github.com/volcengine/volcengine-go-sdk/volcengine"
vesession "github.com/volcengine/volcengine-go-sdk/volcengine/session"
@@ -37,6 +38,9 @@ type DeployerConfig struct {
// 负载均衡监听器 ID。
// 部署目标为 [DEPLOY_TARGET_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
// SNI 域名(支持泛域名)。
// 部署目标为 [DEPLOY_TARGET_LOADBALANCER]、[DEPLOY_TARGET_LISTENER] 时选填。
Domain string `json:"domain,omitempty"`
}
type Deployer struct {
@@ -194,6 +198,42 @@ func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) err
}
func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
// 查询指定监听器的详细信息
// REF: https://www.volcengine.com/docs/6406/71778
describeListenerAttributesReq := &veclb.DescribeListenerAttributesInput{
ListenerId: ve.String(cloudListenerId),
}
describeListenerAttributesResp, err := d.sdkClient.DescribeListenerAttributesWithContext(ctx, describeListenerAttributesReq)
d.logger.Debug("sdk request 'clb.DescribeListenerAttributes'", slog.Any("request", describeListenerAttributesReq), slog.Any("response", describeListenerAttributesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'clb.DescribeListenerAttributes': %w", err)
}
if describeListenerAttributesResp == nil {
return fmt.Errorf("could not find clb listener '%s'", cloudListenerId)
}
if d.config.Domain == "" {
// 未指定 SNI,只需部署到监听器
if ve.StringValue(describeListenerAttributesResp.CertCenterCertificateId) == cloudCertId {
d.logger.Info("no need to deploy clb listener default certificate")
return nil
}
return d.updateListenerDefaultCertificate(ctx, cloudListenerId, cloudCertId)
} else {
if lo.SomeBy(describeListenerAttributesResp.DomainExtensions, func(domainExtension *veclb.DomainExtensionForDescribeListenerAttributesOutput) bool {
return ve.StringValue(domainExtension.Domain) == d.config.Domain && ve.StringValue(domainExtension.CertCenterCertificateId) == cloudCertId
}) {
d.logger.Info("no need to deploy clb listener sni certificate")
return nil
}
// 指定 SNI,需部署到扩展域名
return d.updateListenerSniCertificate(ctx, describeListenerAttributesResp, cloudCertId)
}
}
func (d *Deployer) updateListenerDefaultCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
// 修改指定监听器
// REF: https://www.volcengine.com/docs/6406/71775
modifyListenerAttributesReq := &veclb.ModifyListenerAttributesInput{
@@ -210,6 +250,35 @@ func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerI
return nil
}
func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListenerInfo *veclb.DescribeListenerAttributesOutput, cloudCertId string) error {
domainExtension, _ := lo.Find(cloudListenerInfo.DomainExtensions, func(domainExtension *veclb.DomainExtensionForDescribeListenerAttributesOutput) bool {
return d.config.Domain == ve.StringValue(domainExtension.Domain)
})
if domainExtension == nil {
return fmt.Errorf("could not find clb listener domain extension '%s' for listener '%s'", d.config.Domain, ve.StringValue(cloudListenerInfo.ListenerId))
}
// 修改指定监听器的扩展域名证书
// REF: https://www.volcengine.com/docs/6406/2193110
modifyListenerDomainExtensionsReq := &veclb.ModifyListenerDomainExtensionsInput{
ListenerId: cloudListenerInfo.ListenerId,
ModifyDomainExtensions: []*veclb.ModifyDomainExtensionForModifyListenerDomainExtensionsInput{
{
DomainExtensionId: domainExtension.DomainExtensionId,
CertificateSource: ve.String("cert_center"),
CertCenterCertificateId: ve.String(cloudCertId),
},
},
}
modifyListenerDomainExtensionsResp, err := d.sdkClient.ModifyListenerDomainExtensionsWithContext(ctx, modifyListenerDomainExtensionsReq)
d.logger.Debug("sdk request 'clb.ModifyListenerDomainExtensions'", slog.Any("request", modifyListenerDomainExtensionsReq), slog.Any("response", modifyListenerDomainExtensionsResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'clb.ModifyListenerDomainExtensions': %w", err)
}
return nil
}
func createSDKClient(accessKeyId, secretAccessKey, region string) (*veclb.CLB, error) {
config := ve.NewConfig().
WithAkSk(accessKeyId, secretAccessKey).
@@ -0,0 +1,99 @@
package v20200309
import (
"context"
"errors"
antiddos "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/antiddos/v20200309"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
)
const APIVersion = antiddos.APIVersion
type Client struct {
common.Client
}
func NewClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *Client, err error) {
client = &Client{}
client.Init(region).
WithCredential(credential).
WithProfile(clientProfile)
return
}
func NewDescribeL7RulesBySSLCertIdRequest() (request *DescribeL7RulesBySSLCertIdRequest) {
return antiddos.NewDescribeL7RulesBySSLCertIdRequest()
}
func NewDescribeL7RulesBySSLCertIdResponse() (response *DescribeL7RulesBySSLCertIdResponse) {
return antiddos.NewDescribeL7RulesBySSLCertIdResponse()
}
func (c *Client) DescribeL7RulesBySSLCertIdWithContext(ctx context.Context, request *DescribeL7RulesBySSLCertIdRequest) (response *DescribeL7RulesBySSLCertIdResponse, err error) {
if request == nil {
request = NewDescribeL7RulesBySSLCertIdRequest()
}
c.InitBaseRequest(&request.BaseRequest, "antiddos", APIVersion, "DescribeL7RulesBySSLCertId")
if c.GetCredential() == nil {
return nil, errors.New("DescribeL7RulesBySSLCertId require credential")
}
request.SetContext(ctx)
response = NewDescribeL7RulesBySSLCertIdResponse()
err = c.Send(request, response)
return
}
func NewDescribeNewL7RulesRequest() (request *DescribeNewL7RulesRequest) {
return antiddos.NewDescribeNewL7RulesRequest()
}
func NewDescribeNewL7RulesResponse() (response *DescribeNewL7RulesResponse) {
return antiddos.NewDescribeNewL7RulesResponse()
}
func (c *Client) DescribeNewL7RulesWithContext(ctx context.Context, request *DescribeNewL7RulesRequest) (response *DescribeNewL7RulesResponse, err error) {
if request == nil {
request = NewDescribeNewL7RulesRequest()
}
c.InitBaseRequest(&request.BaseRequest, "antiddos", APIVersion, "DescribeNewL7Rules")
if c.GetCredential() == nil {
return nil, errors.New("DescribeNewL7Rules require credential")
}
request.SetContext(ctx)
response = NewDescribeNewL7RulesResponse()
err = c.Send(request, response)
return
}
func NewModifyNewDomainRulesRequest() (request *ModifyNewDomainRulesRequest) {
return antiddos.NewModifyNewDomainRulesRequest()
}
func NewModifyNewDomainRulesResponse() (response *ModifyNewDomainRulesResponse) {
return antiddos.NewModifyNewDomainRulesResponse()
}
func (c *Client) ModifyNewDomainRulesWithContext(ctx context.Context, request *ModifyNewDomainRulesRequest) (response *ModifyNewDomainRulesResponse, err error) {
if request == nil {
request = NewModifyNewDomainRulesRequest()
}
c.InitBaseRequest(&request.BaseRequest, "antiddos", APIVersion, "ModifyNewDomainRules")
if c.GetCredential() == nil {
return nil, errors.New("ModifyNewDomainRules require credential")
}
request.SetContext(ctx)
response = NewModifyNewDomainRulesResponse()
err = c.Send(request, response)
return
}
@@ -0,0 +1,21 @@
package v20200309
import (
antiddos "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/antiddos/v20200309"
)
type (
NewL7RuleEntry = antiddos.NewL7RuleEntry
)
type DescribeL7RulesBySSLCertIdRequest = antiddos.DescribeL7RulesBySSLCertIdRequest
type DescribeL7RulesBySSLCertIdResponse = antiddos.DescribeL7RulesBySSLCertIdResponse
type DescribeNewL7RulesRequest = antiddos.DescribeNewL7RulesRequest
type DescribeNewL7RulesResponse = antiddos.DescribeNewL7RulesResponse
type ModifyNewDomainRulesRequest = antiddos.ModifyNewDomainRulesRequest
type ModifyNewDomainRulesResponse = antiddos.ModifyNewDomainRulesResponse
@@ -0,0 +1,39 @@
package clb
import (
"github.com/volcengine/volcengine-go-sdk/service/clb"
"github.com/volcengine/volcengine-go-sdk/volcengine"
"github.com/volcengine/volcengine-go-sdk/volcengine/request"
)
const opDescribeListenerAttributes = "DescribeListenerAttributes"
func (c *CLB) DescribeListenerAttributesRequest(input *DescribeListenerAttributesInput) (req *request.Request, output *DescribeListenerAttributesOutput) {
op := &request.Operation{
Name: opDescribeListenerAttributes,
HTTPMethod: "GET",
HTTPPath: "/",
}
if input == nil {
input = &DescribeListenerAttributesInput{}
}
output = &DescribeListenerAttributesOutput{}
req = c.newRequest(op, input, output)
return
}
func (c *CLB) DescribeListenerAttributesWithContext(ctx volcengine.Context, input *DescribeListenerAttributesInput, opts ...request.Option) (*DescribeListenerAttributesOutput, error) {
req, out := c.DescribeListenerAttributesRequest(input)
req.SetContext(ctx)
req.ApplyOptions(opts...)
return out, req.Send()
}
type DescribeListenerAttributesInput = clb.DescribeListenerAttributesInput
type DescribeListenerAttributesOutput = clb.DescribeListenerAttributesOutput
type DomainExtensionForDescribeListenerAttributesOutput = clb.DomainExtensionForDescribeListenerAttributesOutput
@@ -0,0 +1,39 @@
package clb
import (
"github.com/volcengine/volcengine-go-sdk/service/clb"
"github.com/volcengine/volcengine-go-sdk/volcengine"
"github.com/volcengine/volcengine-go-sdk/volcengine/request"
)
const opModifyListenerDomainExtensions = "ModifyListenerDomainExtensions"
func (c *CLB) ModifyListenerDomainExtensionsRequest(input *ModifyListenerDomainExtensionsInput) (req *request.Request, output *ModifyListenerDomainExtensionsOutput) {
op := &request.Operation{
Name: opModifyListenerDomainExtensions,
HTTPMethod: "GET",
HTTPPath: "/",
}
if input == nil {
input = &ModifyListenerDomainExtensionsInput{}
}
output = &ModifyListenerDomainExtensionsOutput{}
req = c.newRequest(op, input, output)
return
}
func (c *CLB) ModifyListenerDomainExtensionsWithContext(ctx volcengine.Context, input *ModifyListenerDomainExtensionsInput, opts ...request.Option) (*ModifyListenerDomainExtensionsOutput, error) {
req, out := c.ModifyListenerDomainExtensionsRequest(input)
req.SetContext(ctx)
req.ApplyOptions(opts...)
return out, req.Send()
}
type ModifyListenerDomainExtensionsInput = clb.ModifyListenerDomainExtensionsInput
type ModifyListenerDomainExtensionsOutput = clb.ModifyListenerDomainExtensionsOutput
type ModifyDomainExtensionForModifyListenerDomainExtensionsInput = clb.ModifyDomainExtensionForModifyListenerDomainExtensionsInput
@@ -0,0 +1,38 @@
package asiaispcdn
import (
"context"
"fmt"
"net/http"
)
type CertificateQueryResponse struct {
sdkResponseBase
Data *CertificateDetail `json:"data,omitempty"`
}
func (c *Client) CertificateQuery(certId string) (*CertificateQueryResponse, error) {
return c.CertificateQueryWithContext(context.Background(), certId)
}
func (c *Client) CertificateQueryWithContext(ctx context.Context, certId string) (*CertificateQueryResponse, error) {
if certId == "" {
return nil, fmt.Errorf("sdkerr: bad request: unset certId")
}
httpreq, err := c.newRequest(http.MethodGet, nil, "certificateQuery")
if err != nil {
return nil, err
} else {
httpreq.SetQueryParam("certId", certId)
httpreq.SetContext(ctx)
}
result := &CertificateQueryResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,32 @@
package asiaispcdn
import (
"context"
"net/http"
)
type CertificateQueryListResponse struct {
sdkResponseBase
Data []*Certificate `json:"data,omitempty"`
}
func (c *Client) CertificateQueryList() (*CertificateQueryListResponse, error) {
return c.CertificateQueryListWithContext(context.Background())
}
func (c *Client) CertificateQueryListWithContext(ctx context.Context) (*CertificateQueryListResponse, error) {
httpreq, err := c.newRequest(http.MethodGet, nil, "certificateQueryList")
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &CertificateQueryListResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,39 @@
package asiaispcdn
import (
"context"
"encoding/json"
"net/http"
)
type CertificateUploadRequest struct {
PublicKey *string `json:"publicKey,omitempty"`
PrivateKey *string `json:"privateKey,omitempty"`
Name *string `json:"name,omitempty"`
}
type CertificateUploadResponse struct {
sdkResponseBase
Data json.Number `json:"data,omitempty"`
}
func (c *Client) CertificateUpload(req *CertificateUploadRequest) (*CertificateUploadResponse, error) {
return c.CertificateUploadWithContext(context.Background(), req)
}
func (c *Client) CertificateUploadWithContext(ctx context.Context, req *CertificateUploadRequest) (*CertificateUploadResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, req, "certificateUpload")
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &CertificateUploadResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+38
View File
@@ -0,0 +1,38 @@
package asiaispcdn
import (
"context"
"net/http"
)
type DomainModifyRequest struct {
Domain *string `json:"domain,omitempty"`
Protocol *string `json:"protocol,omitempty"`
CertId *int64 `json:"certId,omitempty"`
}
type DomainModifyResponse struct {
sdkResponseBase
Data int64 `json:"data,omitempty"`
}
func (c *Client) DomainModify(req *DomainModifyRequest) (*DomainModifyResponse, error) {
return c.DomainModifyWithContext(context.Background(), req)
}
func (c *Client) DomainModifyWithContext(ctx context.Context, req *DomainModifyRequest) (*DomainModifyResponse, error) {
httpreq, err := c.newRequest(http.MethodPut, req, "domainModify")
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &DomainModifyResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,46 @@
package asiaispcdn
import (
"context"
"net/http"
qs "github.com/google/go-querystring/query"
)
type DomainQueryListRequest struct {
Domain *string `json:"domain,omitempty" url:"domain,omitempty"`
SubDomain *bool `json:"subDomain,omitempty" url:"subDomain,omitempty"`
LiveDomain *bool `json:"liveDomain,omitempty" url:"liveDomain,omitempty"`
}
type DomainQueryListResponse struct {
sdkResponseBase
Data []*Domain `json:"data,omitempty"`
}
func (c *Client) DomainQueryList(req *DomainQueryListRequest) (*DomainQueryListResponse, error) {
return c.DomainQueryListWithContext(context.Background(), req)
}
func (c *Client) DomainQueryListWithContext(ctx context.Context, req *DomainQueryListRequest) (*DomainQueryListResponse, error) {
httpreq, err := c.newRequest(http.MethodGet, nil, "domainQueryList")
if err != nil {
return nil, err
} else {
values, err := qs.Values(req)
if err != nil {
return nil, err
}
httpreq.SetQueryParamsFromValues(values)
httpreq.SetContext(ctx)
}
result := &DomainQueryListResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+121
View File
@@ -0,0 +1,121 @@
// A simple SDK client for Asia-ISP CDN.
package asiaispcdn
import (
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/go-resty/resty/v2"
"github.com/certimate-go/certimate/internal/app"
)
type Client struct {
rc *resty.Client
}
func NewClient(optFns ...OptionsFunc) (*Client, error) {
opts := &Options{}
for _, fn := range optFns {
fn(opts)
}
if opts.AccessKeyId == "" {
return nil, fmt.Errorf("sdkerr: unset accessKeyId")
}
if opts.AccessKeySecret == "" {
return nil, fmt.Errorf("sdkerr: unset accessKeySecret")
}
signer := &signer{
accessKeyId: opts.AccessKeyId,
accessKeySecret: opts.AccessKeySecret,
}
httper := resty.New().
SetBaseURL("https://api.asia-isp.com/openapi/v3/stat").
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", app.AppUserAgent).
SetPreRequestHook(func(_ *resty.Client, req *http.Request) error {
if err := signer.Sign(req); err != nil {
return fmt.Errorf("sdkerr: sign error: %w", err)
}
return nil
})
return &Client{rc: httper}, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.rc.SetTimeout(timeout)
return c
}
func (c *Client) SetTLSConfig(config *tls.Config) *Client {
c.rc.SetTLSClientConfig(config)
return c
}
func (c *Client) newRequest(method string, params any, xAction string) (*resty.Request, error) {
if method == "" {
return nil, fmt.Errorf("sdkerr: unset method")
}
if xAction == "" {
return nil, fmt.Errorf("sdkerr: unset action")
}
req := c.rc.R()
req.Method = method
req.SetQueryParam("action", xAction)
req.SetBody(params)
// WARN:
// DO NOT CALL `req.SetBody` or `req.SetFormData` AGAIN! USE `newRequest` INSTEAD.
// DO NOT CALL `req.SetResult` or `req.SetError` AGAIN! USE `doRequestWithResult` INSTEAD.
return req, nil
}
func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
resp, err := c.doRequest(req)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &res)
}
return resp, err
}
if len(resp.Body()) != 0 {
if err := json.Unmarshal(resp.Body(), &res); err != nil {
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String())
} else {
if rCode := res.GetCode(); rCode != 0 {
return resp, fmt.Errorf("sdkerr: api error: code='%d', msg='%s'", rCode, res.GetMsg())
}
}
}
return resp, nil
}
+34
View File
@@ -0,0 +1,34 @@
package asiaispcdn
type Domain struct {
Domain string `json:"domain"`
ServiceType string `json:"serviceType"`
Scope string `json:"scope"`
Cname string `json:"cname"`
ICP string `json:"icp"`
Protocol string `json:"protocol"`
CertId int64 `json:"certId"`
OriginProtocol string `json:"originProtocol"`
OriginHost string `json:"originHost"`
OriginType string `json:"originType"`
OriginAddr string `json:"originAddr"`
OriginBackAddr string `json:"originBackAddr"`
DomainStatus int32 `json:"domainStatus"`
OperatingStatus int32 `json:"operatingStatus"`
}
type Certificate struct {
CertId string `json:"certId"`
Name string `json:"name"`
}
type CertificateDetail struct {
Certificate
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey"`
SubjectName string `json:"subjectName"`
Brand string `json:"brand"`
IssueTime string `json:"issueTime"`
ExpireTime string `json:"expireTime"`
}
+15
View File
@@ -0,0 +1,15 @@
package asiaispcdn
type Options struct {
AccessKeyId string
AccessKeySecret string
}
type OptionsFunc func(*Options)
func WithAkSk(ak, sk string) OptionsFunc {
return func(o *Options) {
o.AccessKeyId = ak
o.AccessKeySecret = sk
}
}
+62
View File
@@ -0,0 +1,62 @@
package asiaispcdn
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net/http"
"strings"
"time"
)
type signer struct {
accessKeyId string
accessKeySecret string
}
func (s *signer) Sign(req *http.Request) error {
method := strings.ToUpper(req.Method)
pathStr := "/"
queryStr := ""
if req.URL != nil {
pathStr = req.URL.Path
queryStr = req.URL.Query().Encode()
}
payloadStr := ""
if method != http.MethodGet && method != http.MethodDelete && req.Body != nil {
payloadb, err := io.ReadAll(req.Body)
if err != nil {
return err
}
payloadStr = string(payloadb)
req.Body = io.NopCloser(bytes.NewReader(payloadb))
}
nonce := 10000000 + rand.Intn(90000000)
timestamp := time.Now().UnixMilli()
stringToSign := ""
if method == http.MethodGet || method == http.MethodDelete {
stringToSign = fmt.Sprintf("accessKeySecret=%s&method=%s&nonce=%d&queryString=%s&timestamp=%d&uri=%s", s.accessKeySecret, method, nonce, queryStr, timestamp, pathStr)
} else {
stringToSign = fmt.Sprintf("accessKeySecret=%s&body=%s&method=%s&nonce=%d&queryString=%s&timestamp=%d&uri=%s", s.accessKeySecret, payloadStr, method, nonce, queryStr, timestamp, pathStr)
}
h := hmac.New(sha1.New, []byte(s.accessKeySecret))
h.Write([]byte(stringToSign))
signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
req.Header.Set("accessKeyId", s.accessKeyId)
req.Header.Set("nonce", fmt.Sprintf("%d", nonce))
req.Header.Set("timestamp", fmt.Sprintf("%d", timestamp))
req.Header.Set("signature", signature)
return nil
}
+36
View File
@@ -0,0 +1,36 @@
package asiaispcdn
import "encoding/json"
type sdkResponse interface {
GetCode() int
GetMsg() string
}
type sdkResponseBase struct {
Code *json.Number `json:"code,omitempty"`
Msg *string `json:"msg,omitempty"`
}
func (r *sdkResponseBase) GetCode() int {
if r.Code.String() == "" {
return 0
}
code, err := r.Code.Int64()
if err != nil {
return -1
}
return int(code)
}
func (r *sdkResponseBase) GetMsg() string {
if r.Msg == nil {
return ""
}
return *r.Msg
}
var _ sdkResponse = (*sdkResponseBase)(nil)
@@ -22,11 +22,11 @@ func (c *Client) CreateDomainWithCert(req *CreateDomainWithCertRequest) (*Create
}
func (c *Client) CreateDomainWithCertWithContext(ctx context.Context, req *CreateDomainWithCertRequest) (*CreateDomainWithCertResponse, error) {
if err := c.ensureApiUserToken(ctx); err != nil {
if err := c.ensureConsoleToken(ctx); err != nil {
return nil, err
}
resp := &CreateDomainWithCertResponse{}
err := c.sendRequestWithResult(ctx, http.MethodPost, "/host/create-domain-with-cert", req, resp)
err := c.sendConsoleRequestWithResult(ctx, http.MethodPost, "/host/create-domain-with-cert", req, resp)
return resp, err
}
+321 -145
View File
@@ -5,8 +5,14 @@ import (
"context"
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"regexp"
@@ -19,6 +25,7 @@ import (
"github.com/go-resty/resty/v2"
"github.com/certimate-go/certimate/internal/app"
xcrypto "github.com/certimate-go/certimate/pkg/utils/crypto"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
@@ -26,15 +33,15 @@ type Client struct {
username string
password string
serverlessToken string
serverlessTokenAt time.Time
serverlessTokenMu sync.Mutex
identityToken string
identityTokenAt time.Time
identityTokenMu sync.Mutex
apiUserToken string
apiUserTokenMu sync.Mutex
consoleToken string
consoleTokenMu sync.Mutex
rcForServerless *resty.Client
rcForApiUser *resty.Client
rcForIdentity *resty.Client
rcForConsole *resty.Client
}
const (
@@ -46,6 +53,11 @@ const (
uniConsoleClientSecret = "4c1f7fbf-c732-42b0-ab10-4634a8bbe834"
uniConsoleAppId = "__UNI__unicloud_console"
uniConsoleSpaceId = "dc-6nfabcn6ada8d3dd"
uniPlatform = "web"
)
const (
challengeAlgRSAOAEP256 = "RSA-OAEP-256+A256GCM"
)
func NewClient(optFns ...OptionsFunc) (*Client, error) {
@@ -65,18 +77,18 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) {
username: opts.Username,
password: opts.Password,
}
client.rcForServerless = resty.New().
client.rcForIdentity = resty.New().
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", app.AppUserAgent)
client.rcForApiUser = resty.New().
client.rcForConsole = resty.New().
SetBaseURL("https://unicloud-api.dcloud.net.cn/unicloud/api").
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", app.AppUserAgent).
SetPreRequestHook(func(_ *resty.Client, req *http.Request) error {
if client.apiUserToken != "" {
req.Header.Set("Token", client.apiUserToken)
if client.consoleToken != "" {
req.Header.Set("Token", client.consoleToken)
}
return nil
@@ -86,101 +98,37 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) {
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.rcForServerless.SetTimeout(timeout)
c.rcForIdentity.SetTimeout(timeout)
return c
}
func (c *Client) buildServerlessClientInfo(appId string) (_clientInfo map[string]any, _err error) {
return map[string]any{
"PLATFORM": "web",
"OS": strings.ToUpper(runtime.GOOS),
"APPID": appId,
"DEVICEID": app.AppName,
"LOCALE": "zh-Hans",
"osName": runtime.GOOS,
"appId": appId,
"appName": "uniCloud",
"deviceId": app.AppName,
"deviceType": "pc",
"uniPlatform": "web",
"uniCompilerVersion": "4.45",
"uniRuntimeVersion": "4.45",
}, nil
}
func (c *Client) buildServerlessPayloadInfo(appId, spaceId, target, method, action string, params, data any) (map[string]any, error) {
clientInfo, err := c.buildServerlessClientInfo(appId)
if err != nil {
return nil, err
}
functionArgsParams := make([]any, 0)
if params != nil {
functionArgsParams = append(functionArgsParams, params)
}
functionArgs := map[string]any{
"clientInfo": clientInfo,
"uniIdToken": c.serverlessToken,
}
if method != "" {
functionArgs["method"] = method
functionArgs["params"] = make([]any, 0)
}
if action != "" {
type _obj struct{}
functionArgs["action"] = action
functionArgs["data"] = &_obj{}
}
if params != nil {
functionArgs["params"] = []any{params}
}
if data != nil {
functionArgs["data"] = data
}
jsonb, err := json.Marshal(map[string]any{
"functionTarget": target,
"functionArgs": functionArgs,
})
if err != nil {
return nil, err
}
payload := map[string]any{
"method": "serverless.function.runtime.invoke",
"params": string(jsonb),
"spaceId": spaceId,
"timestamp": time.Now().UnixMilli(),
}
return payload, nil
}
func (c *Client) invokeServerless(ctx context.Context, endpoint, clientSecret, appId, spaceId, target, method, action string, params, data any) (*resty.Response, error) {
func (c *Client) sendIdentityRequest(ctx context.Context, endpoint, clientSecret, appId, spaceId, functionTarget, functionMethod, functionAction string, functionParams, functionData any) (*resty.Response, error) {
if endpoint == "" {
return nil, fmt.Errorf("sdkerr: bad request: endpoint cannot be empty")
}
payload, err := c.buildServerlessPayloadInfo(appId, spaceId, target, method, action, params, data)
payloadInfo, clientInfo, err := buildServerlessPayloadInfo(appId, spaceId, functionTarget, functionMethod, functionAction, functionParams, functionData, c.identityToken)
if err != nil {
return nil, fmt.Errorf("sdkerr: bad request: failed to build request: %w", err)
}
clientInfo, _ := c.buildServerlessClientInfo(appId)
clientInfoJsonb, _ := json.Marshal(clientInfo)
sign := generateSignature(payloadInfo, clientSecret)
sign := generateSignature(payload, clientSecret)
req := c.rcForServerless.R().
req := c.rcForIdentity.R().
SetHeader("Content-Type", "application/json").
SetHeader("Origin", "https://unicloud.dcloud.net.cn").
SetHeader("Referer", "https://unicloud.dcloud.net.cn").
SetHeader("X-Client-Info", string(clientInfoJsonb)).
SetHeader("X-Client-Token", c.serverlessToken).
SetHeader("Origin", "https://account.dcloud.net.cn").
SetHeader("Referer", "https://account.dcloud.net.cn").
SetHeader("X-Serverless-Sign", sign).
SetBody(payload).
SetBody(payloadInfo).
SetContext(ctx)
if clientInfo != nil {
clientInfoBytes, _ := json.Marshal(clientInfo)
req.SetHeader("X-Client-Info", string(clientInfoBytes))
}
if c.identityToken != "" {
req.SetHeader("X-Client-Token", c.identityToken)
}
resp, err := req.Post(endpoint)
if err != nil {
return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
@@ -191,8 +139,8 @@ func (c *Client) invokeServerless(ctx context.Context, endpoint, clientSecret, a
return resp, nil
}
func (c *Client) invokeServerlessWithResult(ctx context.Context, endpoint, clientSecret, appId, spaceId, target, method, action string, params, data any, result sdkResponse) error {
resp, err := c.invokeServerless(ctx, endpoint, clientSecret, appId, spaceId, target, method, action, params, data)
func (c *Client) sendIdentityRequestWithResult(ctx context.Context, endpoint, clientSecret, appId, spaceId, functionTarget, functionMethod, functionAction string, functionParams, functionData any, result sdkResponse) error {
resp, err := c.sendIdentityRequest(ctx, endpoint, clientSecret, appId, spaceId, functionTarget, functionMethod, functionAction, functionParams, functionData)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &result)
@@ -209,8 +157,8 @@ func (c *Client) invokeServerlessWithResult(ctx context.Context, endpoint, clien
return nil
}
func (c *Client) sendRequest(ctx context.Context, method string, path string, params any) (*resty.Response, error) {
req := c.rcForApiUser.R().
func (c *Client) sendConsoleRequest(ctx context.Context, method string, path string, params any) (*resty.Response, error) {
req := c.rcForConsole.R().
SetContext(ctx)
if strings.EqualFold(method, http.MethodGet) {
qs := make(map[string]string)
@@ -240,8 +188,8 @@ func (c *Client) sendRequest(ctx context.Context, method string, path string, pa
return resp, nil
}
func (c *Client) sendRequestWithResult(ctx context.Context, method string, path string, params any, result sdkResponse) error {
resp, err := c.sendRequest(ctx, method, path, params)
func (c *Client) sendConsoleRequestWithResult(ctx context.Context, method string, path string, params any, result sdkResponse) error {
resp, err := c.sendConsoleRequest(ctx, method, path, params)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &result)
@@ -252,73 +200,146 @@ func (c *Client) sendRequestWithResult(ctx context.Context, method string, path
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("sdkerr: failed to unmarshal response: %w", err)
} else if rReturnCode := result.GetReturnCode(); rReturnCode != 0 {
return fmt.Errorf("sdkerr: ret='%d', desc='%s'", rReturnCode, result.GetReturnDesc())
return fmt.Errorf("sdkerr: api error: ret='%d', desc='%s'", rReturnCode, result.GetReturnDesc())
}
return nil
}
func (c *Client) ensureServerlessToken(ctx context.Context) error {
c.serverlessTokenMu.Lock()
defer c.serverlessTokenMu.Unlock()
if c.serverlessToken != "" && c.serverlessTokenAt.After(time.Now()) {
func (c *Client) ensureIdentityToken(ctx context.Context) error {
c.identityTokenMu.Lock()
defer c.identityTokenMu.Unlock()
if c.identityToken != "" && c.identityTokenAt.After(time.Now()) {
return nil
}
params := map[string]string{
"password": "password",
}
if regexp.MustCompile(`^1\d{10}$`).MatchString(c.username) {
params["mobile"] = c.username
} else if regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`).MatchString(c.username) {
params["email"] = c.username
} else {
params["username"] = c.username
}
challangeId := ""
challengeVer := 0
challengeAlg := ""
challengeKey := ""
challengeKeyId := ""
type loginResponse struct {
sdkResponseBase
Data *struct {
Code int32 `json:"errCode"`
UID string `json:"uid"`
NewToken *struct {
Token string `json:"token"`
TokenExpired int64 `json:"tokenExpired"`
} `json:"newToken,omitempty"`
} `json:"data,omitempty"`
}
resp := &loginResponse{}
if err := c.invokeServerlessWithResult(ctx,
uniIdentityEndpoint, uniIdentityClientSecret, uniIdentityAppId, uniIdentitySpaceId,
"uni-id-co", "login", "", params, nil,
resp); err != nil {
return err
} else {
if resp.Data == nil || resp.Data.NewToken == nil || resp.Data.NewToken.Token == "" {
return fmt.Errorf("sdkerr: auth error: received empty token")
// 生成密码挑战
{
createPasswordChallengeParams := map[string]any{
"action": "login",
"resetAppId": uniConsoleAppId,
"resetUniPlatform": uniPlatform,
}
tokenAt := time.UnixMilli(resp.Data.NewToken.TokenExpired)
if tokenAt.IsZero() {
return fmt.Errorf("sdkerr: auth error: received invalid token expiration")
createPasswordChallengeResp := &struct {
sdkResponseBase
Data *struct {
ErrCode json.RawMessage `json:"errCode,omitempty"`
ErrMsg string `json:"errMsg,omitempty"`
Version int `json:"version"`
Algorithm string `json:"algorithm"`
KeyId string `json:"keyId"`
PublicKey string `json:"publicKey"`
ChallengeId string `json:"challengeId"`
ExpiresAt int64 `json:"expiresAt"`
} `json:"data,omitempty"`
}{}
if err := c.sendIdentityRequestWithResult(ctx,
uniIdentityEndpoint, uniIdentityClientSecret, uniIdentityAppId, uniIdentitySpaceId,
"uni-id-co", "createPasswordChallenge", "", createPasswordChallengeParams, nil,
createPasswordChallengeResp); err != nil {
return err
} else {
if createPasswordChallengeResp.Data != nil && createPasswordChallengeResp.Data.ErrCode != nil {
respErrCode := strings.Trim(string(createPasswordChallengeResp.Data.ErrCode), "\"")
if respErrCode != "" && respErrCode != "0" {
return fmt.Errorf("sdkerr: auth error: errCode='%s', errMsg='%s'", respErrCode, createPasswordChallengeResp.Data.ErrMsg)
}
}
if createPasswordChallengeResp.Data == nil || createPasswordChallengeResp.Data.ChallengeId == "" {
return fmt.Errorf("sdkerr: auth error: received empty challenge")
}
challangeId = createPasswordChallengeResp.Data.ChallengeId
challengeVer = createPasswordChallengeResp.Data.Version
challengeAlg = createPasswordChallengeResp.Data.Algorithm
challengeKey = createPasswordChallengeResp.Data.PublicKey
challengeKeyId = createPasswordChallengeResp.Data.KeyId
}
}
// 登录
{
envelope := make(map[string]any)
switch challengeAlg {
case challengeAlgRSAOAEP256:
envelopeInfo, err := buildPasswordEnvelopeWithRSAOAEP256AES256GCM(c.username, c.password, challangeId, challengeVer, challengeKey, challengeKeyId)
if err != nil {
return fmt.Errorf("sdkerr: auth error: failed to build password envelope: %w", err)
}
envelope = envelopeInfo
default:
return fmt.Errorf("sdkerr: auth error: unsupported challenge algorithm: %s", challengeAlg)
}
c.serverlessToken = resp.Data.NewToken.Token
c.serverlessTokenAt = tokenAt
loginParams := map[string]any{
"captcha": "",
"resetAppId": uniConsoleAppId,
"resetUniPlatform": uniPlatform,
"isReturnToken": false,
"passwordEnvelope": envelope,
}
loginResp := &struct {
sdkResponseBase
Data *struct {
ErrCode json.RawMessage `json:"errCode,omitempty"`
ErrMsg string `json:"errMsg,omitempty"`
UID string `json:"uid"`
NewToken *struct {
Token string `json:"token"`
TokenExpired int64 `json:"tokenExpired"`
} `json:"newToken,omitempty"`
} `json:"data,omitempty"`
}{}
if err := c.sendIdentityRequestWithResult(ctx,
uniIdentityEndpoint, uniIdentityClientSecret, uniIdentityAppId, uniIdentitySpaceId,
"uni-id-co", "login", "", loginParams, nil,
loginResp); err != nil {
return err
} else {
if loginResp.Data != nil && loginResp.Data.ErrCode != nil {
respErrCode := strings.Trim(string(loginResp.Data.ErrCode), "\"")
if respErrCode != "" && respErrCode != "0" {
return fmt.Errorf("sdkerr: auth error: errCode='%s', errMsg='%s'", respErrCode, loginResp.Data.ErrMsg)
}
}
if loginResp.Data == nil || loginResp.Data.NewToken == nil || loginResp.Data.NewToken.Token == "" {
return fmt.Errorf("sdkerr: auth error: received empty token")
}
tokenAt := time.UnixMilli(loginResp.Data.NewToken.TokenExpired)
if tokenAt.IsZero() {
return fmt.Errorf("sdkerr: auth error: received invalid token expiration")
}
c.identityToken = loginResp.Data.NewToken.Token
c.identityTokenAt = tokenAt
}
}
return nil
}
func (c *Client) ensureApiUserToken(ctx context.Context) error {
if err := c.ensureServerlessToken(ctx); err != nil {
func (c *Client) ensureConsoleToken(ctx context.Context) error {
if err := c.ensureIdentityToken(ctx); err != nil {
return err
}
c.apiUserTokenMu.Lock()
defer c.apiUserTokenMu.Unlock()
if c.apiUserToken != "" {
c.consoleTokenMu.Lock()
defer c.consoleTokenMu.Unlock()
if c.consoleToken != "" {
return nil
}
@@ -338,7 +359,7 @@ func (c *Client) ensureApiUserToken(ctx context.Context) error {
}
resp := &getUserTokenResponse{}
if err := c.invokeServerlessWithResult(ctx,
if err := c.sendIdentityRequestWithResult(ctx,
uniConsoleEndpoint, uniConsoleClientSecret, uniConsoleAppId, uniConsoleSpaceId,
"uni-cloud-kernel", "", "user/getUserToken", nil, map[string]any{"isLogin": true},
resp); err != nil {
@@ -348,12 +369,167 @@ func (c *Client) ensureApiUserToken(ctx context.Context) error {
return fmt.Errorf("sdkerr: auth error: received empty token")
}
c.apiUserToken = resp.Data.Data.Data.Token
c.consoleToken = resp.Data.Data.Data.Token
}
return nil
}
func buildServerlessClientInfo(appId string) (map[string]any, error) {
deviceId := strings.ToLower(app.AppName)
return map[string]any{
"APPID": appId,
"DEVICEID": deviceId,
"LOCALE": "zh-Hans",
"OS": strings.ToLower(runtime.GOOS),
"PLATFORM": uniPlatform,
"appId": appId,
"appLanguage": "zh-Hans",
"appName": "uniCloud",
"appVersion": "1.0.0",
"appVersionCode": "100",
"deviceId": deviceId,
"deviceModel": "PC",
"deviceType": "pc",
"locale": "zh-Hans",
"osName": runtime.GOOS,
"osVersion": runtime.GOARCH,
"scene": 1001,
"uniPlatform": uniPlatform,
"uniCompilerVersion": "5.23",
"uniRuntimeVersion": "5.23",
}, nil
}
func buildServerlessPayloadInfo(appId, spaceId, functionTarget, functionMethod, functionAction string, functionParams, functionData any, uniIdToken string) (map[string]any, map[string]any, error) {
clientInfo, err := buildServerlessClientInfo(appId)
if err != nil {
return nil, nil, err
}
functionArgsParams := make([]any, 0)
if functionParams != nil {
functionArgsParams = append(functionArgsParams, functionParams)
}
functionArgs := map[string]any{
"clientInfo": clientInfo,
}
if functionMethod != "" {
functionArgs["method"] = functionMethod
functionArgs["params"] = make([]any, 0)
}
if functionAction != "" {
type _obj struct{}
functionArgs["action"] = functionAction
functionArgs["data"] = &_obj{}
}
if functionParams != nil {
functionArgs["params"] = []any{functionParams}
}
if functionData != nil {
functionArgs["data"] = functionData
}
if uniIdToken != "" {
functionArgs["uniIdToken"] = uniIdToken
}
jsonb, err := json.Marshal(map[string]any{
"functionTarget": functionTarget,
"functionArgs": functionArgs,
})
if err != nil {
return nil, nil, err
}
payloadInfo := map[string]any{
"method": "serverless.function.runtime.invoke",
"params": string(jsonb),
"spaceId": spaceId,
"timestamp": time.Now().UnixMilli(),
}
return payloadInfo, clientInfo, nil
}
func buildPasswordEnvelopeWithRSAOAEP256AES256GCM(username, password string, challengeId string, challengeVer int, challengeKey string, challengeKeyId string) (map[string]any, error) {
plain := map[string]any{
"action": "login",
"challengeId": challengeId,
"issuedAt": time.Now().UnixMilli(),
"username": username,
"password": password,
}
if regexp.MustCompile(`^1\d{10}$`).MatchString(username) {
plain["mobile"] = username
delete(plain, "username")
} else if regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`).MatchString(username) {
plain["email"] = username
delete(plain, "username")
}
plainBytes, err := json.Marshal(plain)
if err != nil {
return nil, err
}
keyBytes := make([]byte, 32)
if _, err := rand.Read(keyBytes); err != nil {
return nil, err
}
ivBytes := make([]byte, 12)
if _, err := rand.Read(ivBytes); err != nil {
return nil, err
}
aad := map[string]any{
"version": challengeVer,
"algorithm": challengeAlgRSAOAEP256,
"keyId": challengeKeyId,
"challengeId": challengeId,
}
// 务必是固定字段顺序,否则会登录报错。因此这里暂时写死 JSON 字符串拼接。
// aadBytes, err := json.Marshal(aad)
// if err != nil {
// return nil, err
// }
aadBytes := fmt.Appendf(nil, `{"version":%d,"algorithm":"%s","keyId":"%s","challengeId":"%s"}`, challengeVer, challengeAlgRSAOAEP256, challengeKeyId, challengeId)
cryptor := xcrypto.NewAESCryptor(keyBytes)
cipherBytes, err := cryptor.GCMEncrypt(plainBytes, ivBytes, aadBytes)
if err != nil {
return nil, err
}
const tagSize int = 16
tagBytes := cipherBytes[len(cipherBytes)-tagSize:]
cipherBytes = cipherBytes[:len(cipherBytes)-tagSize]
rsaPubKeyPemBlock, _ := pem.Decode([]byte(challengeKey))
if rsaPubKeyPemBlock == nil || (rsaPubKeyPemBlock.Type != "PUBLIC KEY") {
return nil, fmt.Errorf("invalid public key")
}
rsaPubKey, err := x509.ParsePKIXPublicKey(rsaPubKeyPemBlock.Bytes)
if err != nil {
return nil, err
}
encKeyBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaPubKey.(*rsa.PublicKey), keyBytes, nil)
if err != nil {
return nil, err
}
envelope := map[string]any{
"encryptedKey": base64.RawStdEncoding.EncodeToString(encKeyBytes),
"ciphertext": base64.RawStdEncoding.EncodeToString(cipherBytes),
"iv": base64.RawStdEncoding.EncodeToString(ivBytes),
"tag": base64.RawStdEncoding.EncodeToString(tagBytes),
}
xmaps.CopyTo(aad, envelope)
return envelope, nil
}
func generateSignature(params map[string]any, secret string) string {
keys := xmaps.Keys(params)
sort.Strings(keys)
@@ -1,4 +1,4 @@
package client
package v3
import (
"context"
@@ -28,8 +28,10 @@ func (c *Client) UpdateCertificateWithContext(ctx context.Context, certId int64,
return nil, fmt.Errorf("sdkerr: bad request: unset certId")
}
if err := c.ensureToken(ctx); err != nil {
return nil, err
if !c.isUseApiKey() {
if err := c.ensureToken(ctx); err != nil {
return nil, err
}
}
httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/certificate/%d", certId))
@@ -1,5 +1,5 @@
// A simple SDK client for LeCDN v3 as user.
package client
package v3
import (
"context"
@@ -20,6 +20,7 @@ import (
type Client struct {
username string
password string
apiKey string
token string
tokenMu sync.Mutex
@@ -39,22 +40,32 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) {
if _, err := url.Parse(serverUrl); err != nil {
return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err)
}
if opts.Username == "" {
return nil, fmt.Errorf("sdkerr: unset username")
if opts.ApiKey == "" && (opts.Username == "" || opts.Password == "") {
return nil, fmt.Errorf("sdkerr: unset apiKey or username/password")
}
if opts.Password == "" {
return nil, fmt.Errorf("sdkerr: unset password")
if opts.ApiKey != "" && (opts.Username != "" || opts.Password != "") {
return nil, fmt.Errorf("sdkerr: cannot set both apiKey and username/password")
}
baseUrl := strings.TrimSuffix(serverUrl, "/")
if opts.ApiKey != "" {
baseUrl += "/api/client"
} else {
baseUrl += "/prod-api"
}
client := &Client{
username: opts.Username,
password: opts.Password,
apiKey: opts.ApiKey,
}
client.rc = resty.New().
SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/prod-api").
SetBaseURL(baseUrl).
SetHeader("User-Agent", app.AppUserAgent).
SetPreRequestHook(func(_ *resty.Client, req *http.Request) error {
if client.token != "" {
if client.apiKey != "" {
req.Header.Set("Authorization", client.apiKey)
} else if client.token != "" {
req.Header.Set("Authorization", "Bearer "+client.token)
}
@@ -123,7 +134,7 @@ func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*rest
if err := json.Unmarshal(resp.Body(), &res); err != nil {
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String())
} else {
if rCode := res.GetCode(); rCode != 200 {
if rCode := res.GetCode(); rCode != 0 && rCode != 200 {
return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", rCode, res.GetMessage())
}
}
@@ -173,3 +184,7 @@ func (c *Client) ensureToken(ctx context.Context) error {
return nil
}
func (c *Client) isUseApiKey() bool {
return c.apiKey != ""
}
@@ -1,8 +1,9 @@
package master
package v3
type Options struct {
Username string
Password string
ApiKey string
}
type OptionsFunc func(*Options)
@@ -11,5 +12,14 @@ func WithLogins(username, password string) OptionsFunc {
return func(o *Options) {
o.Username = username
o.Password = password
o.ApiKey = ""
}
}
func WithApiKey(apiKey string) OptionsFunc {
return func(o *Options) {
o.ApiKey = apiKey
o.Username = ""
o.Password = ""
}
}
@@ -1,4 +1,4 @@
package client
package v3
type sdkResponse interface {
GetCode() int
@@ -0,0 +1,50 @@
package v4
import (
"context"
"fmt"
"net/http"
)
type UpdateCertificateRequest struct {
Name string `json:"name"`
Description string `json:"description"`
SSLPEM string `json:"ssl_pem"`
SSLKey string `json:"ssl_key"`
AutoRenewal bool `json:"auto_renewal"`
}
type UpdateCertificateResponse struct {
sdkResponseBase
}
func (c *Client) UpdateCertificate(certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
return c.UpdateCertificateWithContext(context.Background(), certId, req)
}
func (c *Client) UpdateCertificateWithContext(ctx context.Context, certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
if certId == 0 {
return nil, fmt.Errorf("sdkerr: bad request: unset certId")
}
if !c.isUseApiKey() {
if err := c.ensureToken(ctx); err != nil {
return nil, err
}
}
httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/certificate/%d", certId))
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &UpdateCertificateResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+190
View File
@@ -0,0 +1,190 @@
// A simple SDK client for LeCDN v4 as user.
package v4
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/go-resty/resty/v2"
"github.com/certimate-go/certimate/internal/app"
)
type Client struct {
username string
password string
apiKey string
token string
tokenMu sync.Mutex
rc *resty.Client
}
func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) {
opts := &Options{}
for _, fn := range optFns {
fn(opts)
}
if serverUrl == "" {
return nil, fmt.Errorf("sdkerr: unset serverUrl")
}
if _, err := url.Parse(serverUrl); err != nil {
return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err)
}
if opts.ApiKey == "" && (opts.Username == "" || opts.Password == "") {
return nil, fmt.Errorf("sdkerr: unset apiKey or username/password")
}
if opts.ApiKey != "" && (opts.Username != "" || opts.Password != "") {
return nil, fmt.Errorf("sdkerr: cannot set both apiKey and username/password")
}
baseUrl := strings.TrimSuffix(serverUrl, "/")
if opts.ApiKey != "" {
baseUrl += "/api/client"
} else {
baseUrl += "/prod-api"
}
client := &Client{
username: opts.Username,
password: opts.Password,
apiKey: opts.ApiKey,
}
client.rc = resty.New().
SetBaseURL(baseUrl).
SetHeader("User-Agent", app.AppUserAgent).
SetPreRequestHook(func(_ *resty.Client, req *http.Request) error {
if client.apiKey != "" {
req.Header.Set("Authorization", client.apiKey)
} else if client.token != "" {
req.Header.Set("Authorization", "Bearer "+client.token)
}
return nil
})
return client, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.rc.SetTimeout(timeout)
return c
}
func (c *Client) SetTLSConfig(config *tls.Config) *Client {
c.rc.SetTLSClientConfig(config)
return c
}
func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
if method == "" {
return nil, fmt.Errorf("sdkerr: unset method")
}
if path == "" {
return nil, fmt.Errorf("sdkerr: unset path")
}
req := c.rc.R()
req.Method = method
req.URL = path
// WARN:
// DO NOT CALL `req.SetResult` or `req.SetError` AGAIN! USE `doRequestWithResult` INSTEAD.
return req, nil
}
func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
resp, err := c.doRequest(req)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &res)
}
return resp, err
}
if len(resp.Body()) != 0 {
if err := json.Unmarshal(resp.Body(), &res); err != nil {
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String())
} else {
if rCode := res.GetCode(); rCode != 0 && rCode != 200 {
return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", rCode, res.GetMessage())
}
}
}
return resp, nil
}
func (c *Client) ensureToken(ctx context.Context) error {
c.tokenMu.Lock()
defer c.tokenMu.Unlock()
if c.token != "" {
return nil
}
httpreq, err := c.newRequest(http.MethodPost, "/auth/login")
if err != nil {
return err
} else {
httpreq.SetBody(map[string]string{
"email": c.username,
"username": c.username,
"password": c.password,
})
httpreq.SetContext(ctx)
}
type loginResponse struct {
sdkResponseBase
Data *struct {
UserId int64 `json:"user_id"`
Username string `json:"username"`
Token string `json:"token"`
} `json:"data,omitempty"`
}
result := &loginResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return err
} else {
if result.Data == nil || result.Data.Token == "" {
return fmt.Errorf("sdkerr: auth error: received empty token")
}
c.token = result.Data.Token
}
return nil
}
func (c *Client) isUseApiKey() bool {
return c.apiKey != ""
}
@@ -1,8 +1,9 @@
package client
package v4
type Options struct {
Username string
Password string
ApiKey string
}
type OptionsFunc func(*Options)
@@ -11,5 +12,14 @@ func WithLogins(username, password string) OptionsFunc {
return func(o *Options) {
o.Username = username
o.Password = password
o.ApiKey = ""
}
}
func WithApiKey(apiKey string) OptionsFunc {
return func(o *Options) {
o.ApiKey = apiKey
o.Username = ""
o.Password = ""
}
}
@@ -1,4 +1,4 @@
package master
package v4
type sdkResponse interface {
GetCode() int
@@ -1,4 +1,4 @@
package master
package v3
import (
"context"
@@ -29,8 +29,10 @@ func (c *Client) UpdateCertificateWithContext(ctx context.Context, certId int64,
return nil, fmt.Errorf("sdkerr: bad request: unset certId")
}
if err := c.ensureToken(ctx); err != nil {
return nil, err
if !c.isUseApiKey() {
if err := c.ensureToken(ctx); err != nil {
return nil, err
}
}
httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/certificate/%d", certId))
@@ -1,5 +1,5 @@
// A simple SDK client for LeCDN v3 as admin.
package master
package v3
import (
"context"
@@ -20,6 +20,7 @@ import (
type Client struct {
username string
password string
apiKey string
token string
tokenMu sync.Mutex
@@ -39,24 +40,34 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) {
if _, err := url.Parse(serverUrl); err != nil {
return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err)
}
if opts.Username == "" {
return nil, fmt.Errorf("sdkerr: unset username")
if opts.ApiKey == "" && (opts.Username == "" || opts.Password == "") {
return nil, fmt.Errorf("sdkerr: unset apiKey or username/password")
}
if opts.Password == "" {
return nil, fmt.Errorf("sdkerr: unset password")
if opts.ApiKey != "" && (opts.Username != "" || opts.Password != "") {
return nil, fmt.Errorf("sdkerr: cannot set both apiKey and username/password")
}
baseUrl := strings.TrimSuffix(serverUrl, "/")
if opts.ApiKey != "" {
baseUrl += "/api/admin"
} else {
baseUrl += "/prod-api"
}
client := &Client{
username: opts.Username,
password: opts.Password,
apiKey: opts.ApiKey,
}
client.rc = resty.New().
SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/prod-api").
SetBaseURL(baseUrl).
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", app.AppUserAgent).
SetPreRequestHook(func(_ *resty.Client, req *http.Request) error {
if client.token != "" {
if client.apiKey != "" {
req.Header.Set("Authorization", client.apiKey)
} else if client.token != "" {
req.Header.Set("Authorization", "Bearer "+client.token)
}
@@ -125,7 +136,7 @@ func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*rest
if err := json.Unmarshal(resp.Body(), &res); err != nil {
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String())
} else {
if rCode := res.GetCode(); rCode != 200 {
if rCode := res.GetCode(); rCode != 0 && rCode != 200 {
return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", rCode, res.GetMessage())
}
}
@@ -174,3 +185,7 @@ func (c *Client) ensureToken(ctx context.Context) error {
return nil
}
func (c *Client) isUseApiKey() bool {
return c.apiKey != ""
}
+25
View File
@@ -0,0 +1,25 @@
package v3
type Options struct {
Username string
Password string
ApiKey string
}
type OptionsFunc func(*Options)
func WithLogins(username, password string) OptionsFunc {
return func(o *Options) {
o.Username = username
o.Password = password
o.ApiKey = ""
}
}
func WithApiKey(apiKey string) OptionsFunc {
return func(o *Options) {
o.ApiKey = apiKey
o.Username = ""
o.Password = ""
}
}
+21
View File
@@ -0,0 +1,21 @@
package v3
type sdkResponse interface {
GetCode() int
GetMessage() string
}
type sdkResponseBase struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (r *sdkResponseBase) GetCode() int {
return r.Code
}
func (r *sdkResponseBase) GetMessage() string {
return r.Message
}
var _ sdkResponse = (*sdkResponseBase)(nil)
@@ -0,0 +1,50 @@
package v4
import (
"context"
"fmt"
"net/http"
)
type UpdateCertificateRequest struct {
Name string `json:"name"`
Description string `json:"description"`
SSLPEM string `json:"ssl_pem"`
SSLKey string `json:"ssl_key"`
AutoRenewal bool `json:"auto_renewal"`
}
type UpdateCertificateResponse struct {
sdkResponseBase
}
func (c *Client) UpdateCertificate(certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
return c.UpdateCertificateWithContext(context.Background(), certId, req)
}
func (c *Client) UpdateCertificateWithContext(ctx context.Context, certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
if certId == 0 {
return nil, fmt.Errorf("sdkerr: bad request: unset certId")
}
if !c.isUseApiKey() {
if err := c.ensureToken(ctx); err != nil {
return nil, err
}
}
httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/certificate/%d", certId))
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &UpdateCertificateResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+191
View File
@@ -0,0 +1,191 @@
// A simple SDK client for LeCDN v4 as admin.
package v4
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/go-resty/resty/v2"
"github.com/certimate-go/certimate/internal/app"
)
type Client struct {
username string
password string
apiKey string
token string
tokenMu sync.Mutex
rc *resty.Client
}
func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) {
opts := &Options{}
for _, fn := range optFns {
fn(opts)
}
if serverUrl == "" {
return nil, fmt.Errorf("sdkerr: unset serverUrl")
}
if _, err := url.Parse(serverUrl); err != nil {
return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err)
}
if opts.ApiKey == "" && (opts.Username == "" || opts.Password == "") {
return nil, fmt.Errorf("sdkerr: unset apiKey or username/password")
}
if opts.ApiKey != "" && (opts.Username != "" || opts.Password != "") {
return nil, fmt.Errorf("sdkerr: cannot set both apiKey and username/password")
}
baseUrl := strings.TrimSuffix(serverUrl, "/")
if opts.ApiKey != "" {
baseUrl += "/api/admin"
} else {
baseUrl += "/prod-api"
}
client := &Client{
username: opts.Username,
password: opts.Password,
apiKey: opts.ApiKey,
}
client.rc = resty.New().
SetBaseURL(baseUrl).
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", app.AppUserAgent).
SetPreRequestHook(func(_ *resty.Client, req *http.Request) error {
if client.apiKey != "" {
req.Header.Set("Authorization", client.apiKey)
} else if client.token != "" {
req.Header.Set("Authorization", "Bearer "+client.token)
}
return nil
})
return client, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.rc.SetTimeout(timeout)
return c
}
func (c *Client) SetTLSConfig(config *tls.Config) *Client {
c.rc.SetTLSClientConfig(config)
return c
}
func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
if method == "" {
return nil, fmt.Errorf("sdkerr: unset method")
}
if path == "" {
return nil, fmt.Errorf("sdkerr: unset path")
}
req := c.rc.R()
req.Method = method
req.URL = path
// WARN:
// DO NOT CALL `req.SetResult` or `req.SetError` AGAIN! USE `doRequestWithResult` INSTEAD.
return req, nil
}
func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
resp, err := c.doRequest(req)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &res)
}
return resp, err
}
if len(resp.Body()) != 0 {
if err := json.Unmarshal(resp.Body(), &res); err != nil {
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String())
} else {
if rCode := res.GetCode(); rCode != 0 && rCode != 200 {
return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", rCode, res.GetMessage())
}
}
}
return resp, nil
}
func (c *Client) ensureToken(ctx context.Context) error {
c.tokenMu.Lock()
defer c.tokenMu.Unlock()
if c.token != "" {
return nil
}
httpreq, err := c.newRequest(http.MethodPost, "/auth/login")
if err != nil {
return err
} else {
httpreq.SetBody(map[string]string{
"username": c.username,
"password": c.password,
})
httpreq.SetContext(ctx)
}
type loginResponse struct {
sdkResponseBase
Data *struct {
UserId int64 `json:"user_id"`
Username string `json:"username"`
Token string `json:"token"`
} `json:"data,omitempty"`
}
result := &loginResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return err
} else {
if result.Data == nil || result.Data.Token == "" {
return fmt.Errorf("sdkerr: auth error: received empty token")
}
c.token = result.Data.Token
}
return nil
}
func (c *Client) isUseApiKey() bool {
return c.apiKey != ""
}
+25
View File
@@ -0,0 +1,25 @@
package v4
type Options struct {
Username string
Password string
ApiKey string
}
type OptionsFunc func(*Options)
func WithLogins(username, password string) OptionsFunc {
return func(o *Options) {
o.Username = username
o.Password = password
o.ApiKey = ""
}
}
func WithApiKey(apiKey string) OptionsFunc {
return func(o *Options) {
o.ApiKey = apiKey
o.Username = ""
o.Password = ""
}
}
+21
View File
@@ -0,0 +1,21 @@
package v4
type sdkResponse interface {
GetCode() int
GetMessage() string
}
type sdkResponseBase struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (r *sdkResponseBase) GetCode() int {
return r.Code
}
func (r *sdkResponseBase) GetMessage() string {
return r.Message
}
var _ sdkResponse = (*sdkResponseBase)(nil)
+4 -1
View File
@@ -41,7 +41,10 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) {
return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err)
}
if opts.JwtToken == "" && (opts.Username == "" || opts.Password == "") {
return nil, fmt.Errorf("sdkerr: unset password or jwtToken")
return nil, fmt.Errorf("sdkerr: unset jwtToken or username/password")
}
if opts.JwtToken != "" && (opts.Username != "" || opts.Password != "") {
return nil, fmt.Errorf("sdkerr: cannot set both jwtToken and username/password")
}
client := &Client{
+3
View File
@@ -12,11 +12,14 @@ func WithLogins(username, password string) OptionsFunc {
return func(o *Options) {
o.Username = username
o.Password = password
o.JwtToken = ""
}
}
func WithJwtToken(jwtToken string) OptionsFunc {
return func(o *Options) {
o.JwtToken = jwtToken
o.Username = ""
o.Password = ""
}
}
+45 -4
View File
@@ -10,21 +10,23 @@ import (
)
type AESCryptor interface {
CBCEncrypt(data []byte) ([]byte, error)
CBCDecrypt(cipher []byte) ([]byte, error)
CBCEncrypt(plaintext []byte) ([]byte, error)
CBCDecrypt(ciphertext []byte) ([]byte, error)
GCMEncrypt(plaintext, iv, aad []byte) ([]byte, error)
GCMDecrypt(ciphertext, iv, aad []byte) ([]byte, error)
}
type aesCryptor struct {
key []byte
}
func (c *aesCryptor) CBCEncrypt(data []byte) ([]byte, error) {
func (c *aesCryptor) CBCEncrypt(plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, err
}
paddedData := c.pkcs7Padding(data, aes.BlockSize)
paddedData := c.pkcs7Padding(plaintext, aes.BlockSize)
ciphertext := make([]byte, aes.BlockSize+len(paddedData))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
@@ -60,6 +62,45 @@ func (c *aesCryptor) CBCDecrypt(ciphertext []byte) ([]byte, error) {
return c.pkcs7Unpadding(ciphertext), nil
}
func (c *aesCryptor) GCMEncrypt(plaintext, iv, aad []byte) ([]byte, error) {
const gcmStandardNonceSize int = 12
if len(iv) != gcmStandardNonceSize {
return nil, fmt.Errorf("the iv length must be %d bytes for GCM", gcmStandardNonceSize)
}
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, err
}
aesGCM, err := cipher.NewGCMWithNonceSize(block, len(iv))
if err != nil {
return nil, err
}
ciphertext := aesGCM.Seal(nil, iv, plaintext, aad)
return ciphertext, nil
}
func (c *aesCryptor) GCMDecrypt(ciphertext, iv, aad []byte) ([]byte, error) {
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
plaintext, err := aesGCM.Open(nil, iv, ciphertext, aad)
if err != nil {
return nil, err
}
return plaintext, nil
}
func (c *aesCryptor) pkcs7Padding(data []byte, blockSize int) []byte {
padding := blockSize - (len(data) % blockSize)
padText := bytes.Repeat([]byte{byte(padding)}, padding)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -14,6 +14,7 @@ import AccessConfigFieldsProviderActalisSSL from "./AccessConfigFieldsProviderAc
import AccessConfigFieldsProviderAkamai from "./AccessConfigFieldsProviderAkamai";
import AccessConfigFieldsProviderAPISIX from "./AccessConfigFieldsProviderAPISIX";
import AccessConfigFieldsProviderArvanCloud from "./AccessConfigFieldsProviderArvanCloud";
import AccessConfigFieldsProviderAsiaISPCDN from "./AccessConfigFieldsProviderAsiaISPCDN";
import AccessConfigFieldsProviderAWS from "./AccessConfigFieldsProviderAWS";
import AccessConfigFieldsProviderAxisNow from "./AccessConfigFieldsProviderAxisNow";
import AccessConfigFieldsProviderAzure from "./AccessConfigFieldsProviderAzure";
@@ -135,6 +136,7 @@ const providerComponentMap: Partial<Record<AccessProviderType, React.ComponentTy
[ACCESS_PROVIDERS.AKAMAI]: AccessConfigFieldsProviderAkamai,
[ACCESS_PROVIDERS.APISIX]: AccessConfigFieldsProviderAPISIX,
[ACCESS_PROVIDERS.ARVANCLOUD]: AccessConfigFieldsProviderArvanCloud,
[ACCESS_PROVIDERS.ASIAISPCDN]: AccessConfigFieldsProviderAsiaISPCDN,
[ACCESS_PROVIDERS.AWS]: AccessConfigFieldsProviderAWS,
[ACCESS_PROVIDERS.AXISNOW]: AccessConfigFieldsProviderAxisNow,
[ACCESS_PROVIDERS.AZURE]: AccessConfigFieldsProviderAzure,
@@ -0,0 +1,62 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderAsiaISPCDN = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
<Form.Item
name={[parentNamePath, "accessKeyId"]}
initialValue={initialValues.accessKeyId}
label={t("access.form.asiaispcdn_access_key_id.label")}
rules={[formRule]}
>
<Input autoComplete="new-password" placeholder={t("access.form.asiaispcdn_access_key_id.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "accessKeySecret"]}
initialValue={initialValues.accessKeySecret}
label={t("access.form.asiaispcdn_access_key_secret.label")}
rules={[formRule]}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.asiaispcdn_access_key_secret.placeholder")} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
accessKeyId: "",
accessKeySecret: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {
const { t: _ } = i18n;
return z.object({
accessKeyId: z.string().nonempty(),
accessKeySecret: z.string().nonempty(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderAsiaISPCDN, {
getInitialValues,
getSchema,
});
export default _default;
@@ -3,8 +3,13 @@ import { Form, Input, Radio, Select, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { useFormNestedFieldsContext } from "./_context";
const AUTH_METHOD_PASSWORD = "password" as const;
const AUTH_METHOD_APIKEY = "apikey" as const;
const AccessConfigFormFieldsProviderLeCDN = () => {
const { i18n, t } = useTranslation();
@@ -13,8 +18,11 @@ const AccessConfigFormFieldsProviderLeCDN = () => {
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const initialValues = getInitialValues();
const fieldAuthMethod = Form.useWatch<string>([parentNamePath, "authMethod"], formInst);
return (
<>
<Form.Item name={[parentNamePath, "serverUrl"]} initialValue={initialValues.serverUrl} label={t("access.form.lecdn_server_url.label")} rules={[formRule]}>
@@ -27,7 +35,7 @@ const AccessConfigFormFieldsProviderLeCDN = () => {
label={t("access.form.lecdn_api_version.label")}
rules={[formRule]}
>
<Select options={["v3"].map((s) => ({ label: s, value: s }))} placeholder={t("access.form.lecdn_api_version.placeholder")} />
<Select options={["v3", "v4"].map((s) => ({ label: s, value: s }))} placeholder={t("access.form.lecdn_api_version.placeholder")} />
</Form.Item>
<Form.Item name={[parentNamePath, "apiRole"]} initialValue={initialValues.apiRole} label={t("access.form.lecdn_api_role.label")} rules={[formRule]}>
@@ -39,13 +47,33 @@ const AccessConfigFormFieldsProviderLeCDN = () => {
/>
</Form.Item>
<Form.Item name={[parentNamePath, "username"]} initialValue={initialValues.username} label={t("access.form.lecdn_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.lecdn_username.placeholder")} />
<Form.Item
name={[parentNamePath, "authMethod"]}
initialValue={initialValues.authMethod}
label={t("access.form.lecdn_auth_method.label")}
rules={[formRule]}
>
<Radio.Group block>
<Radio.Button value={AUTH_METHOD_PASSWORD}>{t("access.form.lecdn_auth_method.option.password.label")}</Radio.Button>
<Radio.Button value={AUTH_METHOD_APIKEY}>{t("access.form.lecdn_auth_method.option.apikey.label")}</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item name={[parentNamePath, "password"]} initialValue={initialValues.password} label={t("access.form.lecdn_password.label")} rules={[formRule]}>
<Input.Password autoComplete="new-password" placeholder={t("access.form.lecdn_password.placeholder")} />
</Form.Item>
<Show when={fieldAuthMethod === AUTH_METHOD_PASSWORD}>
<Form.Item name={[parentNamePath, "username"]} initialValue={initialValues.username} label={t("access.form.lecdn_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.lecdn_username.placeholder")} />
</Form.Item>
<Form.Item name={[parentNamePath, "password"]} initialValue={initialValues.password} label={t("access.form.lecdn_password.label")} rules={[formRule]}>
<Input.Password autoComplete="new-password" placeholder={t("access.form.lecdn_password.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldAuthMethod === AUTH_METHOD_APIKEY}>
<Form.Item name={[parentNamePath, "apiKey"]} initialValue={initialValues.apiKey} label={t("access.form.lecdn_api_key.label")} rules={[formRule]}>
<Input.Password autoComplete="new-password" placeholder={t("access.form.lecdn_api_key.placeholder")} />
</Form.Item>
</Show>
<Form.Item
name={[parentNamePath, "allowInsecureConnections"]}
@@ -62,24 +90,67 @@ const AccessConfigFormFieldsProviderLeCDN = () => {
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
serverUrl: "http://<your-host-addr>:5090/",
apiVersion: "v3",
apiVersion: "v4",
apiRole: "client",
username: "",
password: "",
authMethod: AUTH_METHOD_PASSWORD,
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {
const { t: _ } = i18n;
return z.object({
serverUrl: z.url({ protocol: z.core.regexes.httpProtocol }),
apiVersion: z.enum(["v3"]),
apiRole: z.enum(["client", "master"]),
username: z.string().nonempty(),
password: z.string().nonempty(),
allowInsecureConnections: z.boolean().nullish(),
});
return z
.object({
serverUrl: z.url({ protocol: z.core.regexes.httpProtocol }),
apiVersion: z.enum(["v3", "v4"]),
apiRole: z.enum(["client", "master"]),
authMethod: z.enum([AUTH_METHOD_PASSWORD, AUTH_METHOD_APIKEY]),
username: z.string().nullish(),
password: z.string().nullish(),
apiKey: z.string().nullish(),
allowInsecureConnections: z.boolean().nullish(),
})
.superRefine((values, ctx) => {
switch (values.authMethod) {
case AUTH_METHOD_PASSWORD:
{
const scUsername = z.string().nonempty();
const spUsername = scUsername.safeParse(values.username);
if (!spUsername.success) {
ctx.addIssue({
code: "custom",
message: z.treeifyError(spUsername.error).errors.join(),
path: ["username"],
});
}
const scPassword = z.string().nonempty();
const spPassword = scPassword.safeParse(values.password);
if (!spPassword.success) {
ctx.addIssue({
code: "custom",
message: z.treeifyError(spPassword.error).errors.join(),
path: ["password"],
});
}
}
break;
case AUTH_METHOD_APIKEY:
{
const scApiKey = z.string().nonempty();
const spApiKey = scApiKey.safeParse(values.apiKey);
if (!spApiKey.success) {
ctx.addIssue({
code: "custom",
message: z.treeifyError(spApiKey.error).errors.join(),
path: ["apiKey"],
});
}
}
break;
}
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderLeCDN, {
@@ -20,8 +20,8 @@ import { isTouchSupported } from "@/utils/browser";
import { DegisnerContextProvider } from "./_context";
import { getAllElements } from "./elements";
import NodeRender from "./NodeRender";
import { getAllNodeRegistries } from "./nodes";
import { BranchNode } from "./nodes/_shared";
import { getAllNodeRegistries, isNodeTypeRegistered } from "./nodes";
import { BaseNode } from "./nodes/_shared";
import "./flowgram.css";
export interface DesignerProps {
@@ -110,7 +110,7 @@ const Designer = forwardRef<DesignerInstance, DesignerProps>(
defaultExpanded: true,
},
formMeta: {
render: () => <BranchNode description={type} />,
render: () => <BaseNode description={type} />,
},
};
},
@@ -195,6 +195,12 @@ const Designer = forwardRef<DesignerInstance, DesignerProps>(
if (typeof node === "string") {
node = flowgramEditorRef.current!.document.getNode(node)!;
}
if (!node) {
return Promise.reject(`Node '${node}' not found`);
}
if (!isNodeTypeRegistered(node.type)) {
return Promise.reject(`Node type '${node.type}' is not registered`);
}
const form = node.form;
return form ? form.validate().then((res) => res && !form.state.invalid) : Promise.resolve(true);
@@ -22,6 +22,7 @@ import BizDeployNodeConfigFieldsProviderAliyunOSS from "./BizDeployNodeConfigFie
import BizDeployNodeConfigFieldsProviderAliyunVOD from "./BizDeployNodeConfigFieldsProviderAliyunVOD";
import BizDeployNodeConfigFieldsProviderAliyunWAF from "./BizDeployNodeConfigFieldsProviderAliyunWAF";
import BizDeployNodeConfigFieldsProviderAPISIX from "./BizDeployNodeConfigFieldsProviderAPISIX";
import BizDeployNodeConfigFieldsProviderAsiaISPCDN from "./BizDeployNodeConfigFieldsProviderAsiaISPCDN";
import BizDeployNodeConfigFieldsProviderAWSACM from "./BizDeployNodeConfigFieldsProviderAWSACM";
import BizDeployNodeConfigFieldsProviderAWSALB from "./BizDeployNodeConfigFieldsProviderAWSALB";
import BizDeployNodeConfigFieldsProviderAWSAmplify from "./BizDeployNodeConfigFieldsProviderAWSAmplify";
@@ -102,6 +103,7 @@ import BizDeployNodeConfigFieldsProviderTencentCloudCDN from "./BizDeployNodeCon
import BizDeployNodeConfigFieldsProviderTencentCloudCLB from "./BizDeployNodeConfigFieldsProviderTencentCloudCLB";
import BizDeployNodeConfigFieldsProviderTencentCloudCOS from "./BizDeployNodeConfigFieldsProviderTencentCloudCOS";
import BizDeployNodeConfigFieldsProviderTencentCloudCSS from "./BizDeployNodeConfigFieldsProviderTencentCloudCSS";
import BizDeployNodeConfigFieldsProviderTencentCloudDDoSPro from "./BizDeployNodeConfigFieldsProviderTencentCloudDDoSPro";
import BizDeployNodeConfigFieldsProviderTencentCloudECDN from "./BizDeployNodeConfigFieldsProviderTencentCloudECDN";
import BizDeployNodeConfigFieldsProviderTencentCloudEO from "./BizDeployNodeConfigFieldsProviderTencentCloudEO";
import BizDeployNodeConfigFieldsProviderTencentCloudEOMakers from "./BizDeployNodeConfigFieldsProviderTencentCloudEOMakers";
@@ -160,6 +162,7 @@ const providerComponentMap: Partial<Record<DeploymentProviderType, React.Compone
[DEPLOYMENT_PROVIDERS.ALIYUN_VOD]: BizDeployNodeConfigFieldsProviderAliyunVOD,
[DEPLOYMENT_PROVIDERS.ALIYUN_WAF]: BizDeployNodeConfigFieldsProviderAliyunWAF,
[DEPLOYMENT_PROVIDERS.APISIX]: BizDeployNodeConfigFieldsProviderAPISIX,
[DEPLOYMENT_PROVIDERS.ASIAISPCDN]: BizDeployNodeConfigFieldsProviderAsiaISPCDN,
[DEPLOYMENT_PROVIDERS.AWS_ACM]: BizDeployNodeConfigFieldsProviderAWSACM,
[DEPLOYMENT_PROVIDERS.AWS_ALB]: BizDeployNodeConfigFieldsProviderAWSALB,
[DEPLOYMENT_PROVIDERS.AWS_AMPLIFY]: BizDeployNodeConfigFieldsProviderAWSAmplify,
@@ -240,6 +243,7 @@ const providerComponentMap: Partial<Record<DeploymentProviderType, React.Compone
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CLB]: BizDeployNodeConfigFieldsProviderTencentCloudCLB,
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_COS]: BizDeployNodeConfigFieldsProviderTencentCloudCOS,
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CSS]: BizDeployNodeConfigFieldsProviderTencentCloudCSS,
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_DDOSPRO]: BizDeployNodeConfigFieldsProviderTencentCloudDDoSPro,
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_ECDN]: BizDeployNodeConfigFieldsProviderTencentCloudECDN,
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_EO]: BizDeployNodeConfigFieldsProviderTencentCloudEO,
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_EOMAKERS]: BizDeployNodeConfigFieldsProviderTencentCloudEOMakers,
@@ -0,0 +1,107 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Radio } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { isDomain } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const;
const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const;
const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const;
const BizDeployNodeConfigFieldsProviderAsiaISPCDN = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const initialValues = getInitialValues();
const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true });
return (
<>
<Form.Item
name={[parentNamePath, "domainMatchPattern"]}
initialValue={initialValues.domainMatchPattern}
label={t("workflow_node.deploy.form.shared_domain_match_pattern.label")}
extra={
fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (
<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.shared_domain_match_pattern.option.exact.help.wildcard") }}></span>
) : (
void 0
)
}
rules={[formRule]}
>
<Radio.Group
options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({
label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),
value: s,
}))}
/>
</Form.Item>
<Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.asiaispcdn_domain.label")}
rules={[formRule]}
>
<Input placeholder={t("workflow_node.deploy.form.asiaispcdn_domain.placeholder")} />
</Form.Item>
</Show>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,
domain: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z
.object({
domainMatchPattern: z.string().nonempty().default(DOMAIN_MATCH_PATTERN_EXACT),
domain: z.string().nullish(),
})
.superRefine((values, ctx) => {
if (values.domainMatchPattern) {
switch (values.domainMatchPattern) {
case DOMAIN_MATCH_PATTERN_EXACT:
case DOMAIN_MATCH_PATTERN_WILDCARD:
{
const scDomain = z.string().refine((v) => isDomain(v, { allowWildcard: true }), t("common.errmsg.domain_invalid"));
const spDomain = scDomain.safeParse(values.domain);
if (!spDomain.success) {
ctx.addIssue({
code: "custom",
message: z.treeifyError(spDomain.error).errors.join(),
path: ["domain"],
});
}
}
break;
}
}
});
};
const _default = Object.assign(BizDeployNodeConfigFieldsProviderAsiaISPCDN, {
getInitialValues,
getSchema,
});
export default _default;
@@ -0,0 +1,130 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Radio } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { isDomain } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const;
const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const;
const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const;
const BizDeployNodeConfigFieldsProviderTencentCloudDDoSPro = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance<typeof formSchema>();
const initialValues = getInitialValues();
const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true });
return (
<>
<Form.Item
name={[parentNamePath, "endpoint"]}
initialValue={initialValues.endpoint}
label={t("workflow_node.deploy.form.tencentcloud_ddospro_endpoint.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_ddospro_endpoint.tooltip") }}></span>}
>
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_ddospro_endpoint.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "instanceId"]}
initialValue={initialValues.instanceId}
label={t("workflow_node.deploy.form.tencentcloud_ddospro_instance_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_ddospro_instance_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_ddospro_instance_id.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "domainMatchPattern"]}
initialValue={initialValues.domainMatchPattern}
label={t("workflow_node.deploy.form.shared_domain_match_pattern.label")}
extra={
fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (
<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.shared_domain_match_pattern.option.exact.help.wildcard") }}></span>
) : (
void 0
)
}
rules={[formRule]}
>
<Radio.Group
options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({
label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),
value: s,
}))}
/>
</Form.Item>
<Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.tencentcloud_ddospro_domain.label")}
rules={[formRule]}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_ddospro_domain.placeholder")} />
</Form.Item>
</Show>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
instanceId: "",
domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,
domain: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z
.object({
endpoint: z.string().nullish(),
instanceId: z.string().nonempty(),
domainMatchPattern: z.string().nonempty().default(DOMAIN_MATCH_PATTERN_EXACT),
domain: z.string().nullish(),
})
.superRefine((values, ctx) => {
if (values.domainMatchPattern) {
switch (values.domainMatchPattern) {
case DOMAIN_MATCH_PATTERN_EXACT:
case DOMAIN_MATCH_PATTERN_WILDCARD:
{
const scDomain = z.string().refine((v) => isDomain(v, { allowWildcard: true }), t("common.errmsg.domain_invalid"));
const spDomain = scDomain.safeParse(values.domain);
if (!spDomain.success) {
ctx.addIssue({
code: "custom",
message: z.treeifyError(spDomain.error).errors.join(),
path: ["domain"],
});
}
}
break;
}
}
});
};
const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudDDoSPro, {
getInitialValues,
getSchema,
});
export default _default;
@@ -4,6 +4,7 @@ import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { isDomain } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
@@ -73,6 +74,18 @@ const BizDeployNodeConfigFieldsProviderVolcEngineCLB = () => {
<Input placeholder={t("workflow_node.deploy.form.volcengine_clb_listener_id.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldResourceType === DEPLOY_TARGET_LOADBALANCER || fieldResourceType === DEPLOY_TARGET_LISTENER}>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.volcengine_clb_snidomain.label")}
extra={t("workflow_node.deploy.form.volcengine_clb_snidomain.help")}
rules={[formRule]}
>
<Input allowClear placeholder={t("workflow_node.deploy.form.volcengine_clb_snidomain.placeholder")} />
</Form.Item>
</Show>
</>
);
};
@@ -85,7 +98,7 @@ const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t: _ } = i18n;
const { t } = i18n;
return z
.object({
@@ -93,6 +106,13 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
deployTarget: z.enum([DEPLOY_TARGET_LOADBALANCER, DEPLOY_TARGET_LISTENER]),
loadbalancerId: z.string().nullish(),
listenerId: z.string().nullish(),
domain: z
.string()
.nullish()
.refine((v) => {
if (!v) return true;
return isDomain(v, { allowWildcard: true });
}, t("common.errmsg.domain_invalid")),
})
.superRefine((values, ctx) => {
switch (values.deployTarget) {
@@ -0,0 +1,37 @@
import { type FlowNodeType } from "@flowgram.ai/fixed-layout-editor";
import { BizApplyNodeRegistry } from "./BizApplyNodeRegistry";
import { BizDeployNodeRegistry } from "./BizDeployNodeRegistry";
import { BizMonitorNodeRegistry } from "./BizMonitorNodeRegistry";
import { BizNotifyNodeRegistry } from "./BizNotifyNodeRegistry";
import { BizPurgeNodeRegistry } from "./BizPurgeNodeRegistry";
import { BizUploadNodeRegistry } from "./BizUploadNodeRegistry";
import { BranchBlockNodeRegistry, ConditionNodeRegistry } from "./ConditionNode";
import { DelayNodeRegistry } from "./DelayNode";
import { EndNodeRegistry } from "./EndNode";
import { StartNodeRegistry } from "./StartNode";
import { CatchBlockNodeRegistry, TryCatchNodeRegistry } from "./TryCatchNode";
const nodeRegistries = [
StartNodeRegistry,
EndNodeRegistry,
DelayNodeRegistry,
BizApplyNodeRegistry,
BizUploadNodeRegistry,
BizMonitorNodeRegistry,
BizDeployNodeRegistry,
BizPurgeNodeRegistry,
BizNotifyNodeRegistry,
ConditionNodeRegistry,
BranchBlockNodeRegistry,
TryCatchNodeRegistry,
CatchBlockNodeRegistry,
];
export const getAllNodeRegistries = () => {
return [...nodeRegistries];
};
export const isNodeTypeRegistered = (type: FlowNodeType) => {
return nodeRegistries.some((registry) => registry.type === type);
};
@@ -17,6 +17,7 @@ import { mergeCls } from "@/utils/css";
import { type NodeJSON, type NodeRegistry } from "./typings";
import { duplicateNodeJSON } from "../_util";
import { useNodeRenderContext } from "../NodeRenderContext";
import { isNodeTypeRegistered } from "./NodeRegistries";
const useInternalRenamingInput = ({ nodeRender }: { nodeRender: NodeRenderReturnType }) => {
const inputRef = useRef<InputRef>(null);
@@ -98,11 +99,11 @@ const InternalNodeCard = ({
const isActivated = useMemo(() => nodeRenderData.activated || nodeRenderData.lineActivated, [nodeRenderData.activated, nodeRenderData.lineActivated]);
const [isHovering, setIsHovering] = useState(false);
const [isNodeInvalid, setIsNodeInvalid] = useState(false);
const [isNodeInvalid, setIsNodeInvalid] = useState(!isNodeTypeRegistered(nodeRegistry.type));
const isNodeDisabled = useWatchFormValueIn(nodeRender.node, "disabled");
const formState = useWatchFormState(nodeRender.node);
useEffect(() => setIsNodeInvalid(!!formState?.invalid), [formState?.invalid]);
useEffect(() => setIsNodeInvalid(!isNodeTypeRegistered(nodeRegistry.type) || !!formState?.invalid), [formState?.invalid]);
return (
<Card
@@ -1,31 +1,3 @@
import { BizApplyNodeRegistry } from "./BizApplyNodeRegistry";
import { BizDeployNodeRegistry } from "./BizDeployNodeRegistry";
import { BizMonitorNodeRegistry } from "./BizMonitorNodeRegistry";
import { BizNotifyNodeRegistry } from "./BizNotifyNodeRegistry";
import { BizPurgeNodeRegistry } from "./BizPurgeNodeRegistry";
import { BizUploadNodeRegistry } from "./BizUploadNodeRegistry";
import { BranchBlockNodeRegistry, ConditionNodeRegistry } from "./ConditionNode";
import { DelayNodeRegistry } from "./DelayNode";
import { EndNodeRegistry } from "./EndNode";
import { StartNodeRegistry } from "./StartNode";
import { CatchBlockNodeRegistry, TryCatchNodeRegistry } from "./TryCatchNode";
export const getAllNodeRegistries = () => {
return [
StartNodeRegistry,
EndNodeRegistry,
DelayNodeRegistry,
BizApplyNodeRegistry,
BizUploadNodeRegistry,
BizMonitorNodeRegistry,
BizDeployNodeRegistry,
BizPurgeNodeRegistry,
BizNotifyNodeRegistry,
ConditionNodeRegistry,
BranchBlockNodeRegistry,
TryCatchNodeRegistry,
CatchBlockNodeRegistry,
];
};
export { getAllNodeRegistries, isNodeTypeRegistered } from "./NodeRegistries";
export type * from "./typings";
+284 -274
View File
@@ -15,131 +15,134 @@ interface BaseProviderWithAccess<P> extends BaseProvider<P> {
}
// #region AccessProvider
/*
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
export const ACCESS_PROVIDERS = Object.freeze({
["1PANEL"]: "1panel",
["35CN"]: "35cn",
["51DNSCOM"]: "51dnscom",
ACMECA: "acmeca",
ACMEDNS: "acmedns",
ACMEHTTPREQ: "acmehttpreq",
ACTALISSSL: "actalisssl",
AKAMAI: "akamai",
ALIYUN: "aliyun",
APISIX: "apisix",
ARVANCLOUD: "arvancloud",
AWS: "aws",
AXISNOW: "axisnow",
AZURE: "azure",
BAIDUCLOUD: "baiducloud",
BAOTAPANEL: "baotapanel",
BAOTAPANELGO: "baotapanelgo",
BAOTAWAF: "baotawaf",
BEGET: "beget",
BOOKMYNAME: "bookmyname",
BUNNY: "bunny",
BYTEPLUS: "byteplus",
CACHEFLY: "cachefly",
CDNFLY: "cdnfly",
CLOUDFLARE: "cloudflare",
CLOUDNS: "cloudns",
CMCCCLOUD: "cmcccloud",
CONOHAVPS: "conohavps",
CONSTELLIX: "constellix",
CPANEL: "cpanel",
CTCCCLOUD: "ctcccloud",
DESEC: "desec",
DIGICERT: "digicert",
DIGITALOCEAN: "digitalocean",
DINGTALKBOT: "dingtalkbot",
DISCORDBOT: "discordbot",
DNSEXIT: "dnsexit",
DNSLA: "dnsla",
DNSMADEEASY: "dnsmadeeasy",
DOKPLOY: "dokploy",
DUCKDNS: "duckdns",
DYNADOT: "dynadot",
DYNU: "dynu",
DYNV6: "dynv6",
EMAIL: "email",
FLEXCDN: "flexcdn",
FLYIO: "flyio",
FTP: "ftp",
GANDINET: "gandinet",
GCORE: "gcore",
GLOBALSIGNATLAS: "globalsignatlas",
GNAME: "gname",
GODADDY: "godaddy",
GOEDGE: "goedge",
GOOGLECLOUD: "googlecloud",
GOOGLETRUSTSERVICES: "googletrustservices",
HETZNER: "hetzner",
HOSTINGDE: "hostingde",
HOSTINGER: "hostinger",
HUAWEICLOUD: "huaweicloud",
HUAWEIIBMC: "huaweiibmc",
INFOMANIAK: "infomaniak",
IONOS: "ionos",
JDCLOUD: "jdcloud",
KONG: "kong",
LARKBOT: "larkbot",
LECDN: "lecdn",
LETSENCRYPT: "letsencrypt",
LETSENCRYPTSTAGING: "letsencryptstaging",
LINODE: "linode",
LITESSL: "litessl",
LOCAL: "local",
MATRIX: "matrix",
MATTERMOST: "mattermost",
MOHUA: "mohua",
NAMECHEAP: "namecheap",
NAMEDOTCOM: "namedotcom",
NAMESILO: "namesilo",
NETCUP: "netcup",
NETLIFY: "netlify",
NGINXPROXYMANAGER: "nginxproxymanager",
NS1: "ns1",
ORACLECLOUD: "oraclecloud",
OVHCLOUD: "ovhcloud",
PORKBUN: "porkbun",
POWERDNS: "powerdns",
PROXMOXBS: "proxmoxbs",
PROXMOXVE: "proxmoxve",
QINGCLOUD: "qingcloud",
RAINYUN: "rainyun",
RATPANEL: "ratpanel",
REGRU: "regru",
RFC2136: "rfc2136",
RUCENTER: "rucenter",
S3: "s3",
SAFELINE: "safeline",
SAMWAF: "samwaf",
SECTIGO: "sectigo",
SIMPLYCOM: "simplycom",
SLACKBOT: "slackbot",
SPACESHIP: "spaceship",
SSH: "ssh",
SSLCOM: "sslcom",
SYNOLOGYDSM: "synologydsm",
TECHNITIUMDNS: "technitiumdns",
TELEGRAMBOT: "telegrambot",
TENCENTCLOUD: "tencentcloud",
TODAYNIC: "todaynic",
UCLOUD: "ucloud",
UNICLOUD: "unicloud",
VERCEL: "vercel",
VOLCENGINE: "volcengine",
VULTR: "vultr",
WEBHOOK: "webhook",
WECOMBOT: "wecombot",
WESTCN: "westcn",
XINNET: "xinnet",
YANDEXCLOUD: "yandexcloud",
ZEROSSL: "zerossl",
} as const);
export const ACCESS_PROVIDERS = Object.freeze(
/*
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
{
["1PANEL"]: "1panel",
["35CN"]: "35cn",
["51DNSCOM"]: "51dnscom",
ACMECA: "acmeca",
ACMEDNS: "acmedns",
ACMEHTTPREQ: "acmehttpreq",
ACTALISSSL: "actalisssl",
AKAMAI: "akamai",
ALIYUN: "aliyun",
APISIX: "apisix",
ARVANCLOUD: "arvancloud",
ASIAISPCDN: "asiaispcdn",
AWS: "aws",
AXISNOW: "axisnow",
AZURE: "azure",
BAIDUCLOUD: "baiducloud",
BAOTAPANEL: "baotapanel",
BAOTAPANELGO: "baotapanelgo",
BAOTAWAF: "baotawaf",
BEGET: "beget",
BOOKMYNAME: "bookmyname",
BUNNY: "bunny",
BYTEPLUS: "byteplus",
CACHEFLY: "cachefly",
CDNFLY: "cdnfly",
CLOUDFLARE: "cloudflare",
CLOUDNS: "cloudns",
CMCCCLOUD: "cmcccloud",
CONOHAVPS: "conohavps",
CONSTELLIX: "constellix",
CPANEL: "cpanel",
CTCCCLOUD: "ctcccloud",
DESEC: "desec",
DIGICERT: "digicert",
DIGITALOCEAN: "digitalocean",
DINGTALKBOT: "dingtalkbot",
DISCORDBOT: "discordbot",
DNSEXIT: "dnsexit",
DNSLA: "dnsla",
DNSMADEEASY: "dnsmadeeasy",
DOKPLOY: "dokploy",
DUCKDNS: "duckdns",
DYNADOT: "dynadot",
DYNU: "dynu",
DYNV6: "dynv6",
EMAIL: "email",
FLEXCDN: "flexcdn",
FLYIO: "flyio",
FTP: "ftp",
GANDINET: "gandinet",
GCORE: "gcore",
GLOBALSIGNATLAS: "globalsignatlas",
GNAME: "gname",
GODADDY: "godaddy",
GOEDGE: "goedge",
GOOGLECLOUD: "googlecloud",
GOOGLETRUSTSERVICES: "googletrustservices",
HETZNER: "hetzner",
HOSTINGDE: "hostingde",
HOSTINGER: "hostinger",
HUAWEICLOUD: "huaweicloud",
HUAWEIIBMC: "huaweiibmc",
INFOMANIAK: "infomaniak",
IONOS: "ionos",
JDCLOUD: "jdcloud",
KONG: "kong",
LARKBOT: "larkbot",
LECDN: "lecdn",
LETSENCRYPT: "letsencrypt",
LETSENCRYPTSTAGING: "letsencryptstaging",
LINODE: "linode",
LITESSL: "litessl",
LOCAL: "local",
MATRIX: "matrix",
MATTERMOST: "mattermost",
MOHUA: "mohua",
NAMECHEAP: "namecheap",
NAMEDOTCOM: "namedotcom",
NAMESILO: "namesilo",
NETCUP: "netcup",
NETLIFY: "netlify",
NGINXPROXYMANAGER: "nginxproxymanager",
NS1: "ns1",
ORACLECLOUD: "oraclecloud",
OVHCLOUD: "ovhcloud",
PORKBUN: "porkbun",
POWERDNS: "powerdns",
PROXMOXBS: "proxmoxbs",
PROXMOXVE: "proxmoxve",
QINGCLOUD: "qingcloud",
RAINYUN: "rainyun",
RATPANEL: "ratpanel",
REGRU: "regru",
RFC2136: "rfc2136",
RUCENTER: "rucenter",
S3: "s3",
SAFELINE: "safeline",
SAMWAF: "samwaf",
SECTIGO: "sectigo",
SIMPLYCOM: "simplycom",
SLACKBOT: "slackbot",
SPACESHIP: "spaceship",
SSH: "ssh",
SSLCOM: "sslcom",
SYNOLOGYDSM: "synologydsm",
TECHNITIUMDNS: "technitiumdns",
TELEGRAMBOT: "telegrambot",
TENCENTCLOUD: "tencentcloud",
TODAYNIC: "todaynic",
UCLOUD: "ucloud",
UNICLOUD: "unicloud",
VERCEL: "vercel",
VOLCENGINE: "volcengine",
VULTR: "vultr",
WEBHOOK: "webhook",
WECOMBOT: "wecombot",
WESTCN: "westcn",
XINNET: "xinnet",
YANDEXCLOUD: "yandexcloud",
ZEROSSL: "zerossl",
} as const
);
export type AccessProviderType = (typeof ACCESS_PROVIDERS)[keyof typeof ACCESS_PROVIDERS];
@@ -207,6 +210,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.BAOTAPANELGO, "provider.baotapanelgo", "/imgs/providers/baota.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.BAOTAWAF, "provider.baotawaf", "/imgs/providers/baota.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.DOKPLOY, "provider.dokploy", "/imgs/providers/dokploy.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.ASIAISPCDN, "provider.asiaispcdn", "/imgs/providers/asiaispcdn.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.AXISNOW, "provider.axisnow", "/imgs/providers/axisnow.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.CDNFLY, "provider.cdnfly", "/imgs/providers/cdnfly.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.GOEDGE, "provider.goedge", "/imgs/providers/goedge.png", [ACCESS_USAGES.HOSTING]],
@@ -603,155 +607,159 @@ export const acmeHttp01ProvidersMap: Map<ACMEHttp01Provider["type"] | string, AC
// #endregion
// #region DeploymentProvider
/*
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
export const DEPLOYMENT_PROVIDERS = Object.freeze({
["1PANEL"]: `${ACCESS_PROVIDERS["1PANEL"]}`,
["1PANEL_CONSOLE"]: `${ACCESS_PROVIDERS["1PANEL"]}-console`,
ALIYUN_ALB: `${ACCESS_PROVIDERS.ALIYUN}-alb`,
ALIYUN_APIGW: `${ACCESS_PROVIDERS.ALIYUN}-apigw`,
ALIYUN_CAS: `${ACCESS_PROVIDERS.ALIYUN}-cas`,
ALIYUN_CAS_DEPLOY: `${ACCESS_PROVIDERS.ALIYUN}-casdeploy`,
ALIYUN_CDN: `${ACCESS_PROVIDERS.ALIYUN}-cdn`,
ALIYUN_CLB: `${ACCESS_PROVIDERS.ALIYUN}-clb`,
ALIYUN_DCDN: `${ACCESS_PROVIDERS.ALIYUN}-dcdn`,
ALIYUN_DDOSPRO: `${ACCESS_PROVIDERS.ALIYUN}-ddospro`,
ALIYUN_ESA: `${ACCESS_PROVIDERS.ALIYUN}-esa`,
ALIYUN_ESA_SAAS: `${ACCESS_PROVIDERS.ALIYUN}-esasaas`,
ALIYUN_FC: `${ACCESS_PROVIDERS.ALIYUN}-fc`,
ALIYUN_GA: `${ACCESS_PROVIDERS.ALIYUN}-ga`,
ALIYUN_LIVE: `${ACCESS_PROVIDERS.ALIYUN}-live`,
ALIYUN_NLB: `${ACCESS_PROVIDERS.ALIYUN}-nlb`,
ALIYUN_OSS: `${ACCESS_PROVIDERS.ALIYUN}-oss`,
ALIYUN_VOD: `${ACCESS_PROVIDERS.ALIYUN}-vod`,
ALIYUN_WAF: `${ACCESS_PROVIDERS.ALIYUN}-waf`,
APISIX: `${ACCESS_PROVIDERS.APISIX}`,
AWS_ACM: `${ACCESS_PROVIDERS.AWS}-acm`,
AWS_ALB: `${ACCESS_PROVIDERS.AWS}-alb`,
AWS_AMPLIFY: `${ACCESS_PROVIDERS.AWS}-amplify`,
AWS_APIGATEWAY: `${ACCESS_PROVIDERS.AWS}-apigateway`,
AWS_CLB: `${ACCESS_PROVIDERS.AWS}-clb`,
AWS_CLOUDFRONT: `${ACCESS_PROVIDERS.AWS}-cloudfront`,
AWS_IAM: `${ACCESS_PROVIDERS.AWS}-iam`,
AWS_NLB: `${ACCESS_PROVIDERS.AWS}-nlb`,
AXISNOW: `${ACCESS_PROVIDERS.AXISNOW}`,
AZURE_KEYVAULT: `${ACCESS_PROVIDERS.AZURE}-keyvault`,
BAIDUCLOUD_APPBLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-appblb`,
BAIDUCLOUD_BLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-blb`,
BAIDUCLOUD_CDN: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cdn`,
BAIDUCLOUD_CERT: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cert`,
BAOTAPANEL: `${ACCESS_PROVIDERS.BAOTAPANEL}`,
BAOTAPANEL_CONSOLE: `${ACCESS_PROVIDERS.BAOTAPANEL}-console`,
BAOTAPANELGO: `${ACCESS_PROVIDERS.BAOTAPANELGO}`,
BAOTAPANELGO_CONSOLE: `${ACCESS_PROVIDERS.BAOTAPANELGO}-console`,
BAOTAWAF: `${ACCESS_PROVIDERS.BAOTAWAF}`,
BAOTAWAF_CONSOLE: `${ACCESS_PROVIDERS.BAOTAWAF}-console`,
BUNNY_CDN: `${ACCESS_PROVIDERS.BUNNY}-cdn`,
BYTEPLUS_ALB: `${ACCESS_PROVIDERS.BYTEPLUS}-alb`,
BYTEPLUS_APIG: `${ACCESS_PROVIDERS.BYTEPLUS}-apig`,
BYTEPLUS_CDN: `${ACCESS_PROVIDERS.BYTEPLUS}-cdn`,
BYTEPLUS_CERTCENTER: `${ACCESS_PROVIDERS.BYTEPLUS}-certcenter`,
BYTEPLUS_CLB: `${ACCESS_PROVIDERS.BYTEPLUS}-clb`,
BYTEPLUS_MEDIALIVE: `${ACCESS_PROVIDERS.BYTEPLUS}-medialive`,
BYTEPLUS_TOS: `${ACCESS_PROVIDERS.BYTEPLUS}-tos`,
CACHEFLY: `${ACCESS_PROVIDERS.CACHEFLY}`,
CDNFLY: `${ACCESS_PROVIDERS.CDNFLY}`,
CLOUDFLARE_SSL: `${ACCESS_PROVIDERS.CLOUDFLARE}-ssl`,
CMCCCLOUD_CDN: `${ACCESS_PROVIDERS.CMCCCLOUD}-cdn`,
CMCCCLOUD_VLB: `${ACCESS_PROVIDERS.CMCCCLOUD}-vlb`,
CPANEL: `${ACCESS_PROVIDERS.CPANEL}`,
CTCCCLOUD_AO: `${ACCESS_PROVIDERS.CTCCCLOUD}-ao`,
CTCCCLOUD_CDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-cdn`,
CTCCCLOUD_CMS: `${ACCESS_PROVIDERS.CTCCCLOUD}-cms`,
CTCCCLOUD_ELB: `${ACCESS_PROVIDERS.CTCCCLOUD}-elb`,
CTCCCLOUD_FAAS: `${ACCESS_PROVIDERS.CTCCCLOUD}-faas`,
CTCCCLOUD_ICDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-icdn`,
CTCCCLOUD_LVDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-lvdn`,
DIGITALOCEAN_CERTIFICATE: `${ACCESS_PROVIDERS.DIGITALOCEAN}-certificate`,
DOKPLOY: `${ACCESS_PROVIDERS.DOKPLOY}`,
FLEXCDN: `${ACCESS_PROVIDERS.FLEXCDN}`,
FLYIO: `${ACCESS_PROVIDERS.FLYIO}`,
FTP: `${ACCESS_PROVIDERS.FTP}`,
GCORE_CDN: `${ACCESS_PROVIDERS.GCORE}-cdn`,
GOEDGE: `${ACCESS_PROVIDERS.GOEDGE}`,
GOOGLECLOUD_CERTIFICATEMANAGER: `${ACCESS_PROVIDERS.GOOGLECLOUD}-certificatemanager`,
HUAWEICLOUD_APIG: `${ACCESS_PROVIDERS.HUAWEICLOUD}-apig`,
HUAWEICLOUD_AAD: `${ACCESS_PROVIDERS.HUAWEICLOUD}-aad`,
HUAWEICLOUD_CDN: `${ACCESS_PROVIDERS.HUAWEICLOUD}-cdn`,
HUAWEICLOUD_ELB: `${ACCESS_PROVIDERS.HUAWEICLOUD}-elb`,
HUAWEICLOUD_LIVE: `${ACCESS_PROVIDERS.HUAWEICLOUD}-live`,
HUAWEICLOUD_OBS: `${ACCESS_PROVIDERS.HUAWEICLOUD}-obs`,
HUAWEICLOUD_SCM: `${ACCESS_PROVIDERS.HUAWEICLOUD}-scm`,
HUAWEICLOUD_VOD: `${ACCESS_PROVIDERS.HUAWEICLOUD}-vod`,
HUAWEICLOUD_WAF: `${ACCESS_PROVIDERS.HUAWEICLOUD}-waf`,
HUAWEIIBMC: `${ACCESS_PROVIDERS.HUAWEIIBMC}`,
JDCLOUD_ALB: `${ACCESS_PROVIDERS.JDCLOUD}-alb`,
JDCLOUD_CDN: `${ACCESS_PROVIDERS.JDCLOUD}-cdn`,
JDCLOUD_LIVE: `${ACCESS_PROVIDERS.JDCLOUD}-live`,
JDCLOUD_VOD: `${ACCESS_PROVIDERS.JDCLOUD}-vod`,
JDCLOUD_SSL: `${ACCESS_PROVIDERS.JDCLOUD}-ssl`,
JDCLOUD_WAF: `${ACCESS_PROVIDERS.JDCLOUD}-waf`,
KONG: `${ACCESS_PROVIDERS.KONG}`,
LECDN: `${ACCESS_PROVIDERS.LECDN}`,
LINODE_LOS: `${ACCESS_PROVIDERS.LINODE}-los`,
LOCAL: `${ACCESS_PROVIDERS.LOCAL}`,
MOHUA_MVH: `${ACCESS_PROVIDERS.MOHUA}-mvh`,
NETLIFY: `${ACCESS_PROVIDERS.NETLIFY}`,
NGINXPROXYMANAGER: `${ACCESS_PROVIDERS.NGINXPROXYMANAGER}`,
ORACLECLOUD_CERTIFICATESMGMT: `${ACCESS_PROVIDERS.ORACLECLOUD}-certificatesmgmt`,
PROXMOXBS: `${ACCESS_PROVIDERS.PROXMOXBS}`,
PROXMOXVE: `${ACCESS_PROVIDERS.PROXMOXVE}`,
QINGCLOUD_LB: `${ACCESS_PROVIDERS.QINGCLOUD}-lb`,
RAINYUN_RCDN: `${ACCESS_PROVIDERS.RAINYUN}-rcdn`,
RAINYUN_SSLCENTER: `${ACCESS_PROVIDERS.RAINYUN}-sslcenter`,
RATPANEL: `${ACCESS_PROVIDERS.RATPANEL}`,
RATPANEL_CONSOLE: `${ACCESS_PROVIDERS.RATPANEL}-console`,
S3: `${ACCESS_PROVIDERS.S3}`,
SAFELINE: `${ACCESS_PROVIDERS.SAFELINE}`,
SAMWAF: `${ACCESS_PROVIDERS.SAMWAF}`,
SAMWAF_CONSOLE: `${ACCESS_PROVIDERS.SAMWAF}-console`,
SSH: `${ACCESS_PROVIDERS.SSH}`,
SYNOLOGYDSM: `${ACCESS_PROVIDERS.SYNOLOGYDSM}`,
TENCENTCLOUD_CDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cdn`,
TENCENTCLOUD_CLB: `${ACCESS_PROVIDERS.TENCENTCLOUD}-clb`,
TENCENTCLOUD_COS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cos`,
TENCENTCLOUD_CSS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-css`,
TENCENTCLOUD_ECDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ecdn`,
TENCENTCLOUD_EO: `${ACCESS_PROVIDERS.TENCENTCLOUD}-eo`,
TENCENTCLOUD_EOMAKERS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-eomakers`,
TENCENTCLOUD_GAAP: `${ACCESS_PROVIDERS.TENCENTCLOUD}-gaap`,
TENCENTCLOUD_GA2: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ga2`,
TENCENTCLOUD_SCF: `${ACCESS_PROVIDERS.TENCENTCLOUD}-scf`,
TENCENTCLOUD_SSL: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ssl`,
TENCENTCLOUD_SSL_DEPLOY: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ssldeploy`,
TENCENTCLOUD_SSL_UPDATE: `${ACCESS_PROVIDERS.TENCENTCLOUD}-sslupdate`,
TENCENTCLOUD_TSE: `${ACCESS_PROVIDERS.TENCENTCLOUD}-tse`,
TENCENTCLOUD_VOD: `${ACCESS_PROVIDERS.TENCENTCLOUD}-vod`,
TENCENTCLOUD_WAF: `${ACCESS_PROVIDERS.TENCENTCLOUD}-waf`,
UCLOUD_UALB: `${ACCESS_PROVIDERS.UCLOUD}-ualb`,
UCLOUD_UCDN: `${ACCESS_PROVIDERS.UCLOUD}-ucdn`,
UCLOUD_UCLB: `${ACCESS_PROVIDERS.UCLOUD}-uclb`,
UCLOUD_UEWAF: `${ACCESS_PROVIDERS.UCLOUD}-uewaf`,
UCLOUD_UPATHX: `${ACCESS_PROVIDERS.UCLOUD}-upathx`,
UCLOUD_US3: `${ACCESS_PROVIDERS.UCLOUD}-us3`,
UNICLOUD_WEBHOST: `${ACCESS_PROVIDERS.UNICLOUD}-webhost`,
VERCEL: `${ACCESS_PROVIDERS.VERCEL}`,
VOLCENGINE_ALB: `${ACCESS_PROVIDERS.VOLCENGINE}-alb`,
VOLCENGINE_APIG: `${ACCESS_PROVIDERS.VOLCENGINE}-apig`,
VOLCENGINE_CDN: `${ACCESS_PROVIDERS.VOLCENGINE}-cdn`,
VOLCENGINE_CERTCENTER: `${ACCESS_PROVIDERS.VOLCENGINE}-certcenter`,
VOLCENGINE_CLB: `${ACCESS_PROVIDERS.VOLCENGINE}-clb`,
VOLCENGINE_DCDN: `${ACCESS_PROVIDERS.VOLCENGINE}-dcdn`,
VOLCENGINE_IMAGEX: `${ACCESS_PROVIDERS.VOLCENGINE}-imagex`,
VOLCENGINE_LIVE: `${ACCESS_PROVIDERS.VOLCENGINE}-live`,
VOLCENGINE_TOS: `${ACCESS_PROVIDERS.VOLCENGINE}-tos`,
VOLCENGINE_VOD: `${ACCESS_PROVIDERS.VOLCENGINE}-vod`,
VOLCENGINE_WAF: `${ACCESS_PROVIDERS.VOLCENGINE}-waf`,
WEBHOOK: `${ACCESS_PROVIDERS.WEBHOOK}`,
YANDEXCLOUD_CERTIFICATEMANAGER: `${ACCESS_PROVIDERS.YANDEXCLOUD}-certificatemanager`,
} as const);
export const DEPLOYMENT_PROVIDERS = Object.freeze(
/*
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
{
["1PANEL"]: `${ACCESS_PROVIDERS["1PANEL"]}`,
["1PANEL_CONSOLE"]: `${ACCESS_PROVIDERS["1PANEL"]}-console`,
ALIYUN_ALB: `${ACCESS_PROVIDERS.ALIYUN}-alb`,
ALIYUN_APIGW: `${ACCESS_PROVIDERS.ALIYUN}-apigw`,
ALIYUN_CAS: `${ACCESS_PROVIDERS.ALIYUN}-cas`,
ALIYUN_CAS_DEPLOY: `${ACCESS_PROVIDERS.ALIYUN}-casdeploy`,
ALIYUN_CDN: `${ACCESS_PROVIDERS.ALIYUN}-cdn`,
ALIYUN_CLB: `${ACCESS_PROVIDERS.ALIYUN}-clb`,
ALIYUN_DCDN: `${ACCESS_PROVIDERS.ALIYUN}-dcdn`,
ALIYUN_DDOSPRO: `${ACCESS_PROVIDERS.ALIYUN}-ddospro`,
ALIYUN_ESA: `${ACCESS_PROVIDERS.ALIYUN}-esa`,
ALIYUN_ESA_SAAS: `${ACCESS_PROVIDERS.ALIYUN}-esasaas`,
ALIYUN_FC: `${ACCESS_PROVIDERS.ALIYUN}-fc`,
ALIYUN_GA: `${ACCESS_PROVIDERS.ALIYUN}-ga`,
ALIYUN_LIVE: `${ACCESS_PROVIDERS.ALIYUN}-live`,
ALIYUN_NLB: `${ACCESS_PROVIDERS.ALIYUN}-nlb`,
ALIYUN_OSS: `${ACCESS_PROVIDERS.ALIYUN}-oss`,
ALIYUN_VOD: `${ACCESS_PROVIDERS.ALIYUN}-vod`,
ALIYUN_WAF: `${ACCESS_PROVIDERS.ALIYUN}-waf`,
APISIX: `${ACCESS_PROVIDERS.APISIX}`,
ASIAISPCDN: `${ACCESS_PROVIDERS.ASIAISPCDN}`,
AWS_ACM: `${ACCESS_PROVIDERS.AWS}-acm`,
AWS_ALB: `${ACCESS_PROVIDERS.AWS}-alb`,
AWS_AMPLIFY: `${ACCESS_PROVIDERS.AWS}-amplify`,
AWS_APIGATEWAY: `${ACCESS_PROVIDERS.AWS}-apigateway`,
AWS_CLB: `${ACCESS_PROVIDERS.AWS}-clb`,
AWS_CLOUDFRONT: `${ACCESS_PROVIDERS.AWS}-cloudfront`,
AWS_IAM: `${ACCESS_PROVIDERS.AWS}-iam`,
AWS_NLB: `${ACCESS_PROVIDERS.AWS}-nlb`,
AXISNOW: `${ACCESS_PROVIDERS.AXISNOW}`,
AZURE_KEYVAULT: `${ACCESS_PROVIDERS.AZURE}-keyvault`,
BAIDUCLOUD_APPBLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-appblb`,
BAIDUCLOUD_BLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-blb`,
BAIDUCLOUD_CDN: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cdn`,
BAIDUCLOUD_CERT: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cert`,
BAOTAPANEL: `${ACCESS_PROVIDERS.BAOTAPANEL}`,
BAOTAPANEL_CONSOLE: `${ACCESS_PROVIDERS.BAOTAPANEL}-console`,
BAOTAPANELGO: `${ACCESS_PROVIDERS.BAOTAPANELGO}`,
BAOTAPANELGO_CONSOLE: `${ACCESS_PROVIDERS.BAOTAPANELGO}-console`,
BAOTAWAF: `${ACCESS_PROVIDERS.BAOTAWAF}`,
BAOTAWAF_CONSOLE: `${ACCESS_PROVIDERS.BAOTAWAF}-console`,
BUNNY_CDN: `${ACCESS_PROVIDERS.BUNNY}-cdn`,
BYTEPLUS_ALB: `${ACCESS_PROVIDERS.BYTEPLUS}-alb`,
BYTEPLUS_APIG: `${ACCESS_PROVIDERS.BYTEPLUS}-apig`,
BYTEPLUS_CDN: `${ACCESS_PROVIDERS.BYTEPLUS}-cdn`,
BYTEPLUS_CERTCENTER: `${ACCESS_PROVIDERS.BYTEPLUS}-certcenter`,
BYTEPLUS_CLB: `${ACCESS_PROVIDERS.BYTEPLUS}-clb`,
BYTEPLUS_MEDIALIVE: `${ACCESS_PROVIDERS.BYTEPLUS}-medialive`,
BYTEPLUS_TOS: `${ACCESS_PROVIDERS.BYTEPLUS}-tos`,
CACHEFLY: `${ACCESS_PROVIDERS.CACHEFLY}`,
CDNFLY: `${ACCESS_PROVIDERS.CDNFLY}`,
CLOUDFLARE_SSL: `${ACCESS_PROVIDERS.CLOUDFLARE}-ssl`,
CMCCCLOUD_CDN: `${ACCESS_PROVIDERS.CMCCCLOUD}-cdn`,
CMCCCLOUD_VLB: `${ACCESS_PROVIDERS.CMCCCLOUD}-vlb`,
CPANEL: `${ACCESS_PROVIDERS.CPANEL}`,
CTCCCLOUD_AO: `${ACCESS_PROVIDERS.CTCCCLOUD}-ao`,
CTCCCLOUD_CDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-cdn`,
CTCCCLOUD_CMS: `${ACCESS_PROVIDERS.CTCCCLOUD}-cms`,
CTCCCLOUD_ELB: `${ACCESS_PROVIDERS.CTCCCLOUD}-elb`,
CTCCCLOUD_FAAS: `${ACCESS_PROVIDERS.CTCCCLOUD}-faas`,
CTCCCLOUD_ICDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-icdn`,
CTCCCLOUD_LVDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-lvdn`,
DIGITALOCEAN_CERTIFICATE: `${ACCESS_PROVIDERS.DIGITALOCEAN}-certificate`,
DOKPLOY: `${ACCESS_PROVIDERS.DOKPLOY}`,
FLEXCDN: `${ACCESS_PROVIDERS.FLEXCDN}`,
FLYIO: `${ACCESS_PROVIDERS.FLYIO}`,
FTP: `${ACCESS_PROVIDERS.FTP}`,
GCORE_CDN: `${ACCESS_PROVIDERS.GCORE}-cdn`,
GOEDGE: `${ACCESS_PROVIDERS.GOEDGE}`,
GOOGLECLOUD_CERTIFICATEMANAGER: `${ACCESS_PROVIDERS.GOOGLECLOUD}-certificatemanager`,
HUAWEICLOUD_APIG: `${ACCESS_PROVIDERS.HUAWEICLOUD}-apig`,
HUAWEICLOUD_AAD: `${ACCESS_PROVIDERS.HUAWEICLOUD}-aad`,
HUAWEICLOUD_CDN: `${ACCESS_PROVIDERS.HUAWEICLOUD}-cdn`,
HUAWEICLOUD_ELB: `${ACCESS_PROVIDERS.HUAWEICLOUD}-elb`,
HUAWEICLOUD_LIVE: `${ACCESS_PROVIDERS.HUAWEICLOUD}-live`,
HUAWEICLOUD_OBS: `${ACCESS_PROVIDERS.HUAWEICLOUD}-obs`,
HUAWEICLOUD_SCM: `${ACCESS_PROVIDERS.HUAWEICLOUD}-scm`,
HUAWEICLOUD_VOD: `${ACCESS_PROVIDERS.HUAWEICLOUD}-vod`,
HUAWEICLOUD_WAF: `${ACCESS_PROVIDERS.HUAWEICLOUD}-waf`,
HUAWEIIBMC: `${ACCESS_PROVIDERS.HUAWEIIBMC}`,
JDCLOUD_ALB: `${ACCESS_PROVIDERS.JDCLOUD}-alb`,
JDCLOUD_CDN: `${ACCESS_PROVIDERS.JDCLOUD}-cdn`,
JDCLOUD_LIVE: `${ACCESS_PROVIDERS.JDCLOUD}-live`,
JDCLOUD_VOD: `${ACCESS_PROVIDERS.JDCLOUD}-vod`,
JDCLOUD_SSL: `${ACCESS_PROVIDERS.JDCLOUD}-ssl`,
JDCLOUD_WAF: `${ACCESS_PROVIDERS.JDCLOUD}-waf`,
KONG: `${ACCESS_PROVIDERS.KONG}`,
LECDN: `${ACCESS_PROVIDERS.LECDN}`,
LINODE_LOS: `${ACCESS_PROVIDERS.LINODE}-los`,
LOCAL: `${ACCESS_PROVIDERS.LOCAL}`,
MOHUA_MVH: `${ACCESS_PROVIDERS.MOHUA}-mvh`,
NETLIFY: `${ACCESS_PROVIDERS.NETLIFY}`,
NGINXPROXYMANAGER: `${ACCESS_PROVIDERS.NGINXPROXYMANAGER}`,
ORACLECLOUD_CERTIFICATESMGMT: `${ACCESS_PROVIDERS.ORACLECLOUD}-certificatesmgmt`,
PROXMOXBS: `${ACCESS_PROVIDERS.PROXMOXBS}`,
PROXMOXVE: `${ACCESS_PROVIDERS.PROXMOXVE}`,
QINGCLOUD_LB: `${ACCESS_PROVIDERS.QINGCLOUD}-lb`,
RAINYUN_RCDN: `${ACCESS_PROVIDERS.RAINYUN}-rcdn`,
RAINYUN_SSLCENTER: `${ACCESS_PROVIDERS.RAINYUN}-sslcenter`,
RATPANEL: `${ACCESS_PROVIDERS.RATPANEL}`,
RATPANEL_CONSOLE: `${ACCESS_PROVIDERS.RATPANEL}-console`,
S3: `${ACCESS_PROVIDERS.S3}`,
SAFELINE: `${ACCESS_PROVIDERS.SAFELINE}`,
SAMWAF: `${ACCESS_PROVIDERS.SAMWAF}`,
SAMWAF_CONSOLE: `${ACCESS_PROVIDERS.SAMWAF}-console`,
SSH: `${ACCESS_PROVIDERS.SSH}`,
SYNOLOGYDSM: `${ACCESS_PROVIDERS.SYNOLOGYDSM}`,
TENCENTCLOUD_CDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cdn`,
TENCENTCLOUD_CLB: `${ACCESS_PROVIDERS.TENCENTCLOUD}-clb`,
TENCENTCLOUD_COS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cos`,
TENCENTCLOUD_CSS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-css`,
TENCENTCLOUD_DDOSPRO: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ddospro`,
TENCENTCLOUD_ECDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ecdn`,
TENCENTCLOUD_EO: `${ACCESS_PROVIDERS.TENCENTCLOUD}-eo`,
TENCENTCLOUD_EOMAKERS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-eomakers`,
TENCENTCLOUD_GAAP: `${ACCESS_PROVIDERS.TENCENTCLOUD}-gaap`,
TENCENTCLOUD_GA2: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ga2`,
TENCENTCLOUD_SCF: `${ACCESS_PROVIDERS.TENCENTCLOUD}-scf`,
TENCENTCLOUD_SSL: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ssl`,
TENCENTCLOUD_SSL_DEPLOY: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ssldeploy`,
TENCENTCLOUD_SSL_UPDATE: `${ACCESS_PROVIDERS.TENCENTCLOUD}-sslupdate`,
TENCENTCLOUD_TSE: `${ACCESS_PROVIDERS.TENCENTCLOUD}-tse`,
TENCENTCLOUD_VOD: `${ACCESS_PROVIDERS.TENCENTCLOUD}-vod`,
TENCENTCLOUD_WAF: `${ACCESS_PROVIDERS.TENCENTCLOUD}-waf`,
UCLOUD_UALB: `${ACCESS_PROVIDERS.UCLOUD}-ualb`,
UCLOUD_UCDN: `${ACCESS_PROVIDERS.UCLOUD}-ucdn`,
UCLOUD_UCLB: `${ACCESS_PROVIDERS.UCLOUD}-uclb`,
UCLOUD_UEWAF: `${ACCESS_PROVIDERS.UCLOUD}-uewaf`,
UCLOUD_UPATHX: `${ACCESS_PROVIDERS.UCLOUD}-upathx`,
UCLOUD_US3: `${ACCESS_PROVIDERS.UCLOUD}-us3`,
UNICLOUD_WEBHOST: `${ACCESS_PROVIDERS.UNICLOUD}-webhost`,
VERCEL: `${ACCESS_PROVIDERS.VERCEL}`,
VOLCENGINE_ALB: `${ACCESS_PROVIDERS.VOLCENGINE}-alb`,
VOLCENGINE_APIG: `${ACCESS_PROVIDERS.VOLCENGINE}-apig`,
VOLCENGINE_CDN: `${ACCESS_PROVIDERS.VOLCENGINE}-cdn`,
VOLCENGINE_CERTCENTER: `${ACCESS_PROVIDERS.VOLCENGINE}-certcenter`,
VOLCENGINE_CLB: `${ACCESS_PROVIDERS.VOLCENGINE}-clb`,
VOLCENGINE_DCDN: `${ACCESS_PROVIDERS.VOLCENGINE}-dcdn`,
VOLCENGINE_IMAGEX: `${ACCESS_PROVIDERS.VOLCENGINE}-imagex`,
VOLCENGINE_LIVE: `${ACCESS_PROVIDERS.VOLCENGINE}-live`,
VOLCENGINE_TOS: `${ACCESS_PROVIDERS.VOLCENGINE}-tos`,
VOLCENGINE_VOD: `${ACCESS_PROVIDERS.VOLCENGINE}-vod`,
VOLCENGINE_WAF: `${ACCESS_PROVIDERS.VOLCENGINE}-waf`,
WEBHOOK: `${ACCESS_PROVIDERS.WEBHOOK}`,
YANDEXCLOUD_CERTIFICATEMANAGER: `${ACCESS_PROVIDERS.YANDEXCLOUD}-certificatemanager`,
} as const
);
export type DeploymentProviderType = (typeof DEPLOYMENT_PROVIDERS)[keyof typeof DEPLOYMENT_PROVIDERS];
@@ -812,6 +820,7 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_EOMAKERS, "provider.tencentcloud_eomakers", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CLB, "provider.tencentcloud_clb", DEPLOYMENT_CATEGORIES.LOADBALANCE],
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_WAF, "provider.tencentcloud_waf", DEPLOYMENT_CATEGORIES.FIREWALL],
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_DDOSPRO, "provider.tencentcloud_ddospro", DEPLOYMENT_CATEGORIES.FIREWALL],
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CSS, "provider.tencentcloud_css", DEPLOYMENT_CATEGORIES.AV],
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_VOD, "provider.tencentcloud_vod", DEPLOYMENT_CATEGORIES.AV],
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SCF, "provider.tencentcloud_scf", DEPLOYMENT_CATEGORIES.SERVERLESS],
@@ -899,6 +908,7 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
[DEPLOYMENT_PROVIDERS.RAINYUN_SSLCENTER, "provider.rainyun_sslcenter_upload", DEPLOYMENT_CATEGORIES.SSL],
[DEPLOYMENT_PROVIDERS.UNICLOUD_WEBHOST, "provider.unicloud_webhost", DEPLOYMENT_CATEGORIES.WEBSITE],
[DEPLOYMENT_PROVIDERS.MOHUA_MVH, "provider.mohua_mvh", DEPLOYMENT_CATEGORIES.WEBSITE],
[DEPLOYMENT_PROVIDERS.ASIAISPCDN, "provider.asiaispcdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.AXISNOW, "provider.axisnow", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.CDNFLY, "provider.cdnfly", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.FLEXCDN, "provider.flexcdn", DEPLOYMENT_CATEGORIES.CDN],
+24
View File
@@ -220,6 +220,14 @@
"placeholder": "Please enter ArvanCloud API key",
"tooltip": "For more information, see <a href=\"https://docs.arvancloud.ir/en/developer-tools/api/api-key\" target=\"_blank\">https://docs.arvancloud.ir/en/developer-tools/api/api-key</a>"
},
"asiaispcdn_access_key_id": {
"label": "Asia-ISP CDN AccessKeyID",
"placeholder": "Please enter Asia-ISP CDN AccessKeyID"
},
"asiaispcdn_access_key_secret": {
"label": "Asia-ISP CDN AccessKeySecret",
"placeholder": "Please enter Asia-ISP CDN AccessKeySecret"
},
"aws_auth_method": {
"label": "AWS API authentication method",
"placeholder": "Please select AWS API authentication method",
@@ -879,6 +887,18 @@
}
}
},
"lecdn_auth_method": {
"label": "LeCDN API authentication method",
"placeholder": "Please select LeCDN API authentication method",
"option": {
"password": {
"label": "Username & Password"
},
"apikey": {
"label": "API key"
}
}
},
"lecdn_username": {
"label": "LeCDN username",
"placeholder": "Please enter LeCDN username"
@@ -887,6 +907,10 @@
"label": "LeCDN password",
"placeholder": "Please enter LeCDN password"
},
"lecdn_api_key": {
"label": "LeCDN API key",
"placeholder": "Please enter LeCDN API key"
},
"linode_access_token": {
"label": "Linode access token",
"placeholder": "Please enter Linode access token",
+3 -1
View File
@@ -54,7 +54,7 @@
"aliyun_cdn": "Alibaba Cloud - CDN (Content Delivery Network)",
"aliyun_clb": "Alibaba Cloud - CLB (Classic Load Balancer)",
"aliyun_dcdn": "Alibaba Cloud - DCDN (Dynamic Route for Content Delivery Network)",
"aliyun_ddospro": "Alibaba Cloud - Anti-DDoS Proxy",
"aliyun_ddospro": "Alibaba Cloud - Anti-DDoS Pro",
"aliyun_dns": "Alibaba Cloud - DNS",
"aliyun_esa": "Alibaba Cloud - ESA (Edge Security Acceleration)",
"aliyun_esa_saas": "Alibaba Cloud - ESA SaaS Manager (Edge Security Acceleration SaaS)",
@@ -67,6 +67,7 @@
"aliyun_waf": "Alibaba Cloud - WAF (Web Application Firewall)",
"apisix": "Apache APISIX",
"arvancloud": "ArvanCloud",
"asiaispcdn": "Asia-ISP CDN",
"aws": "AWS (Amazon Web Services)",
"aws_acm": "AWS - ACM (Amazon Certificate Manager)",
"aws_alb": "AWS - ALB (Application Load Balancer)",
@@ -249,6 +250,7 @@
"tencentcloud_clb": "Tencent Cloud - CLB (Cloud Load Balancer)",
"tencentcloud_cos": "Tencent Cloud - COS (Cloud Object Storage)",
"tencentcloud_css": "Tencent Cloud - CSS (Cloud Streaming Service)",
"tencentcloud_ddospro": "Tencent Cloud - Anti-DDoS Pro",
"tencentcloud_dns": "Tencent Cloud - DNS",
"tencentcloud_ecdn": "Tencent Cloud - ECDN (Enterprise Content Delivery Network)",
"tencentcloud_eo": "Tencent Cloud - EdgeOne",
@@ -978,6 +978,10 @@
"placeholder": "Please enter APISIX certificate ID",
"tooltip": "You can find it on APISIX dashboard."
},
"asiaispcdn_domain": {
"label": "Asia-ISP CDN domain",
"placeholder": "Please enter Asia-ISP CDN domain name"
},
"aws_acm_region": {
"label": "AWS Region",
"placeholder": "Please enter AWS ACM region (e.g. us-east-1)",
@@ -2190,6 +2194,20 @@
"label": "Tencent Cloud CSS streaming domain",
"placeholder": "Please enter Tencent Cloud CSS streaming domain name"
},
"tencentcloud_ddospro_endpoint": {
"label": "Tencent Cloud API endpoint (Optional)",
"placeholder": "Please enter Tencent Cloud Anti-DDoS Pro API endpoint (e.g. clb.intl.tencentcloudapi.com)",
"tooltip": "<ul style=\"list-style: disc;\"><li><strong>clb.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>clb.tencentcloudapi.com</strong> for Tencent Cloud in China (<em>Default</em>)</li></ul>"
},
"tencentcloud_ddospro_instance_id": {
"label": "Tencent Cloud Anti-DDoS Pro instance ID",
"placeholder": "Please enter Tencent Cloud Anti-DDoS Pro instance ID",
"tooltip": "For more information, see <a href=\"https://console.tencentcloud.com/ddos\" target=\"_blank\">https://console.tencentcloud.com/ddos</a>"
},
"tencentcloud_ddospro_domain": {
"label": "Tencent Cloud Anti-DDoS Pro domain",
"placeholder": "Please enter Tencent Cloud Anti-DDoS Pro domain name"
},
"tencentcloud_ecdn_endpoint": {
"label": "Tencent Cloud API endpoint (Optional)",
"placeholder": "Please enter Tencent Cloud ECDN API endpoint (e.g. cdn.intl.tencentcloudapi.com)",
@@ -2696,6 +2714,11 @@
"placeholder": "Please enter VolcEngine CLB listener ID",
"tooltip": "For more information, see <a href=\"https://console.volcengine.com/clb/LoadBalancer\" target=\"_blank\">https://console.volcengine.com/clb/LoadBalancer</a>"
},
"volcengine_clb_snidomain": {
"label": "VolcEngine CLB SNI domain (Optional)",
"placeholder": "Please enter the VolcEngine CLB extension domain",
"help": "Notes: Leave it blank to set the default certificate; otherwise, set the extension one for SNI."
},
"volcengine_dcdn_domain": {
"label": "VolcEngine DCDN domain",
"placeholder": "Please enter VolcEngine DCDN domain name"
+25 -1
View File
@@ -220,6 +220,14 @@
"placeholder": "请输入 ArvanCloud API 密钥",
"tooltip": "这是什么?请参阅 <a href=\"https://docs.arvancloud.ir/en/developer-tools/api/api-key\" target=\"_blank\">https://docs.arvancloud.ir/en/developer-tools/api/api-key</a>"
},
"asiaispcdn_access_key_id": {
"label": "橙域网络网迹 CDN AccessKeyID",
"placeholder": "请输入橙域网络网迹 CDN AccessKeyID"
},
"asiaispcdn_access_key_secret": {
"label": "橙域网络网迹 CDN AccessKeySecret",
"placeholder": "请输入橙域网络网迹 CDN AccessKeySecret"
},
"aws_auth_method": {
"label": "AWS API 认证方式",
"placeholder": "请选择 AWS API 认证方式",
@@ -879,6 +887,18 @@
}
}
},
"lecdn_auth_method": {
"label": "LeCDN API 认证方式",
"placeholder": "请选择 LeCDN API 认证方式",
"option": {
"password": {
"label": "账号密码"
},
"apikey": {
"label": "API Key"
}
}
},
"lecdn_username": {
"label": "LeCDN 用户名",
"placeholder": "请输入 LeCDN 用户名"
@@ -887,6 +907,10 @@
"label": "LeCDN 用户密码",
"placeholder": "请输入 LeCDN 用户密码"
},
"lecdn_api_key": {
"label": "LeCDN API Key",
"placeholder": "请输入 LeCDN API Key"
},
"linode_access_token": {
"label": "Linode AccessToken",
"placeholder": "请输入 Linode AccessToken",
@@ -996,7 +1020,7 @@
"placeholder": "请选择 NPM API 认证方式",
"option": {
"password": {
"label": "密码"
"label": "账号密码"
},
"token": {
"label": "API Token"
@@ -67,6 +67,7 @@
"aliyun_waf": "阿里云 - Web 应用防火墙 WAF",
"apisix": "Apache APISIX",
"arvancloud": "ArvanCloud",
"asiaispcdn": "橙域网络网迹 CDN",
"aws": "AWS(Amazon Web Services)",
"aws_acm": "AWS - 证书管理器 ACM",
"aws_alb": "AWS - 应用程序负载均衡器 ALB",
@@ -249,6 +250,7 @@
"tencentcloud_clb": "腾讯云 - 负载均衡 CLB",
"tencentcloud_cos": "腾讯云 - 对象存储 COS",
"tencentcloud_css": "腾讯云 - 云直播 CSS",
"tencentcloud_ddospro": "腾讯云 - DDoS 高防",
"tencentcloud_dns": "腾讯云 - 云解析 DNS",
"tencentcloud_ecdn": "腾讯云 - 全站加速网络 ECDN",
"tencentcloud_eo": "腾讯云 - 边缘安全加速平台 EdgeOne",
@@ -977,6 +977,10 @@
"placeholder": "请输入 APISIX 证书 ID",
"tooltip": "请登录 APISIX 控制台查看"
},
"asiaispcdn_domain": {
"label": "橙域网络网迹 CDN 加速域名",
"placeholder": "请输入橙域网络网迹 CDN 加速域名"
},
"aws_acm_region": {
"label": "AWS 服务区域",
"placeholder": "请输入 AWS ACM 服务区域(例如:us-east-1)",
@@ -2189,6 +2193,20 @@
"label": "腾讯云云直播流域名",
"placeholder": "请输入腾讯云云直播流域名"
},
"tencentcloud_ddospro_endpoint": {
"label": "腾讯云接口端点(可选)",
"placeholder": "请输入腾讯云 DDoS 高防接口端点(例如:clb.tencentcloudapi.com)",
"tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/214/30669\" target=\"_blank\">https://cloud.tencent.com/document/product/214/30669</a><br>国际站用户请填写 <em>clb.intl.tencentcloudapi.com</em>。"
},
"tencentcloud_ddospro_instance_id": {
"label": "腾讯云 DDoS 高防实例 ID",
"placeholder": "请输入腾讯云 DDoS 高防实例 ID",
"tooltip": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/ddos\" target=\"_blank\">https://console.cloud.tencent.com/ddos</a>"
},
"tencentcloud_ddospro_domain": {
"label": "腾讯云 DDoS 高防网站域名",
"placeholder": "请输入腾讯云 DDoS 高防网站域名"
},
"tencentcloud_ecdn_endpoint": {
"label": "腾讯云接口端点(可选)",
"placeholder": "请输入腾讯云 ECDN 接口端点(例如:cdn.tencentcloudapi.com)",
@@ -2695,6 +2713,11 @@
"placeholder": "请输入火山引擎 CLB 监听器 ID",
"tooltip": "这是什么?请参阅 <a href=\"https://console.volcengine.com/clb/LoadBalancer\" target=\"_blank\">https://console.volcengine.com/clb/LoadBalancer</a>"
},
"volcengine_clb_snidomain": {
"label": "火山引擎 CLB 扩展域名(可选)",
"placeholder": "请输入火山引擎 CLB 扩展域名",
"help": "提示:不填写时,将替换监听器的默认证书;否则,将替换扩展域名证书。"
},
"volcengine_dcdn_domain": {
"label": "火山引擎 DCDN 加速域名",
"placeholder": "请输入火山引擎 DCDN 加速域名"