diff --git a/README.md b/README.md
index 63d7898b..156e6d7a 100644
--- a/README.md
+++ b/README.md
@@ -74,7 +74,7 @@ make local.run
| 服务商 | 支持申请证书 | 支持部署证书 | 备注 |
| :--------: | :----------: | :----------: | ------------------------------------------------------------ |
| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN |
-| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、CLB |
+| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、ECDN、CLB、TEO |
| 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB |
| 七牛云 | | √ | 可部署到七牛云 CDN |
| AWS | √ | | 可签发在 AWS Route53 托管的域名 |
diff --git a/README_EN.md b/README_EN.md
index 5bb30b38..e1353feb 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -70,22 +70,22 @@ password:1234567890
## List of Supported Providers
-| Provider | Registration | Deployment | Remarks |
-| :-----------: | :----------: | :--------: | ------------------------------------------------------------------------------------------------ |
-| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN |
-| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, CLB |
-| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB |
-| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN |
-| AWS | √ | | Supports domains managed on AWS Route53 |
-| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates |
-| GoDaddy | √ | | Supports domains registered on GoDaddy |
-| Namesilo | √ | | Supports domains registered on Namesilo |
-| PowerDNS | √ | | Supports domains managed on PowerDNS |
-| HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request |
-| Local Deploy | | √ | Supports deployment to local servers |
-| SSH | | √ | Supports deployment to SSH servers |
-| Webhook | | √ | Supports callback to Webhook |
-| Kubernetes | | √ | Supports deployment to Kubernetes Secret |
+| Provider | Registration | Deployment | Remarks |
+| :-----------: | :----------: | :--------: | --------------------------------------------------------------------------------------------------------------------- |
+| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN |
+| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, ECDN, CLB, TEO |
+| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB |
+| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN |
+| AWS | √ | | Supports domains managed on AWS Route53 |
+| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates |
+| GoDaddy | √ | | Supports domains registered on GoDaddy |
+| Namesilo | √ | | Supports domains registered on Namesilo |
+| PowerDNS | √ | | Supports domains managed on PowerDNS |
+| HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request |
+| Local Deploy | | √ | Supports deployment to local servers |
+| SSH | | √ | Supports deployment to SSH servers |
+| Webhook | | √ | Supports callback to Webhook |
+| Kubernetes | | √ | Supports deployment to Kubernetes Secret |
## Screenshots
diff --git a/go.mod b/go.mod
index dff90fd5..d2cf83b2 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
github.com/pocketbase/pocketbase v0.22.18
github.com/qiniu/go-sdk/v7 v7.22.0
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017
- github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017
+ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
golang.org/x/crypto v0.28.0
k8s.io/apimachinery v0.31.1
@@ -54,6 +54,7 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
+ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
diff --git a/go.sum b/go.sum
index f666c84a..17f7c756 100644
--- a/go.sum
+++ b/go.sum
@@ -451,10 +451,14 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017 h1:SXrldOXwgomYuATVAuz5ofpTjB+99qVELgdy5R5kMgI=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030 h1:kwiUoCkooUgy7iPyhEEbio7WT21kGJUeZ5JeJfb/dYk=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002/go.mod h1:WdC0FYbqYhJwQ3kbqri6hVP5HAEp+rzX9FToItTAzUg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992 h1:A6O89OlCJQUpNxGqC/E5By04UNKBryIt5olQIGOx8mg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992/go.mod h1:BcvC7ZPdSlhRggVq4J1ToJlgv8bmODIAuSo0naFZOLo=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030 h1:tlHbfQlAfL12J/5XF4indKl0cAA3vEn6TDiGZVsr050=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030/go.mod h1:8dW6JByZKNDAPnjlXxBk9yDc+QGbldpa0tBRfi1kG+U=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go
index 512fd748..21deb609 100644
--- a/internal/deployer/deployer.go
+++ b/internal/deployer/deployer.go
@@ -19,8 +19,10 @@ const (
targetAliyunCDN = "aliyun-cdn"
targetAliyunESA = "aliyun-dcdn"
targetTencentCDN = "tencent-cdn"
+ targetTencentECDN = "tencent-ecdn"
targetTencentCLB = "tencent-clb"
targetTencentCOS = "tencent-cos"
+ targetTencentTEO = "tencent-teo"
targetHuaweiCloudCDN = "huaweicloud-cdn"
targetHuaweiCloudELB = "huaweicloud-elb"
targetQiniuCdn = "qiniu-cdn"
@@ -107,11 +109,15 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
case targetAliyunESA:
return NewAliyunESADeployer(option)
case targetTencentCDN:
- return NewTencentCDNDeployer(option)
+ return NewTencentCDNDeployer(option)
+ case targetTencentECDN:
+ return NewTencentECDNDeployer(option)
case targetTencentCLB:
return NewTencentCLBDeployer(option)
case targetTencentCOS:
return NewTencentCOSDeployer(option)
+ case targetTencentTEO:
+ return NewTencentTEODeployer(option)
case targetHuaweiCloudCDN:
return NewHuaweiCloudCDNDeployer(option)
case targetHuaweiCloudELB:
diff --git a/internal/deployer/tencent_ecdn.go b/internal/deployer/tencent_ecdn.go
new file mode 100644
index 00000000..ea52794e
--- /dev/null
+++ b/internal/deployer/tencent_ecdn.go
@@ -0,0 +1,146 @@
+package deployer
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
+ "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
+ "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
+ ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
+
+ "github.com/usual2970/certimate/internal/domain"
+ "github.com/usual2970/certimate/internal/utils/rand"
+)
+
+type TencentECDNDeployer struct {
+ option *DeployerOption
+ credential *common.Credential
+ infos []string
+}
+
+func NewTencentECDNDeployer(option *DeployerOption) (Deployer, error) {
+ access := &domain.TencentAccess{}
+ if err := json.Unmarshal([]byte(option.Access), access); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
+ }
+
+ credential := common.NewCredential(
+ access.SecretId,
+ access.SecretKey,
+ )
+
+ return &TencentECDNDeployer{
+ option: option,
+ credential: credential,
+ infos: make([]string, 0),
+ }, nil
+}
+
+func (d *TencentECDNDeployer) GetID() string {
+ return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
+}
+
+func (d *TencentECDNDeployer) GetInfo() []string {
+ return d.infos
+}
+
+func (d *TencentECDNDeployer) Deploy(ctx context.Context) error {
+ // 上传证书
+ certId, err := d.uploadCert()
+ if err != nil {
+ return fmt.Errorf("failed to upload certificate: %w", err)
+ }
+ d.infos = append(d.infos, toStr("上传证书", certId))
+
+ if err := d.deploy(certId); err != nil {
+ return fmt.Errorf("failed to deploy: %w", err)
+ }
+
+ return nil
+}
+
+func (d *TencentECDNDeployer) uploadCert() (string, error) {
+ cpf := profile.NewClientProfile()
+ cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
+
+ client, _ := ssl.NewClient(d.credential, "", cpf)
+
+ request := ssl.NewUploadCertificateRequest()
+
+ request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate)
+ request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey)
+ request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6))
+ request.Repeatable = common.BoolPtr(false)
+
+ response, err := client.UploadCertificate(request)
+ if err != nil {
+ return "", fmt.Errorf("failed to upload certificate: %w", err)
+ }
+
+ return *response.Response.CertificateId, nil
+}
+
+func (d *TencentECDNDeployer) deploy(certId string) error {
+ cpf := profile.NewClientProfile()
+ cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
+ // 实例化要请求产品的client对象,clientProfile是可选的
+ client, _ := ssl.NewClient(d.credential, "", cpf)
+
+ // 实例化一个请求对象,每个接口都会对应一个request对象
+ request := ssl.NewDeployCertificateInstanceRequest()
+
+ request.CertificateId = common.StringPtr(certId)
+ request.ResourceType = common.StringPtr("ecdn")
+ request.Status = common.Int64Ptr(1)
+
+ // 如果是泛域名就从cdn列表下获取SSL证书中的可用域名
+ domain := getDeployString(d.option.DeployConfig, "domain")
+ if strings.Contains(domain, "*") {
+ list, errGetList := d.getDomainList()
+ if errGetList != nil {
+ return fmt.Errorf("failed to get certificate domain list: %w", errGetList)
+ }
+ if list == nil || len(list) == 0 {
+ return fmt.Errorf("failed to get certificate domain list: empty list.")
+ }
+ request.InstanceIdList = common.StringPtrs(list)
+ } else { // 否则直接使用传入的域名
+ request.InstanceIdList = common.StringPtrs([]string{domain})
+ }
+
+ // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
+ resp, err := client.DeployCertificateInstance(request)
+ if err != nil {
+ return fmt.Errorf("failed to deploy certificate: %w", err)
+ }
+ d.infos = append(d.infos, toStr("部署证书", resp.Response))
+ return nil
+}
+
+func (d *TencentECDNDeployer) getDomainList() ([]string, error) {
+ cpf := profile.NewClientProfile()
+ cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com"
+ client, _ := cdn.NewClient(d.credential, "", cpf)
+
+ request := cdn.NewDescribeCertDomainsRequest()
+
+ cert := base64.StdEncoding.EncodeToString([]byte(d.option.Certificate.Certificate))
+ request.Cert = &cert
+ request.Product = common.StringPtr("ecdn")
+
+ response, err := client.DescribeCertDomains(request)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get domain list: %w", err)
+ }
+
+ domains := make([]string, 0)
+ for _, domain := range response.Response.Domains {
+ domains = append(domains, *domain)
+ }
+
+ return domains, nil
+}
diff --git a/internal/deployer/tencent_teo.go b/internal/deployer/tencent_teo.go
new file mode 100644
index 00000000..930232ef
--- /dev/null
+++ b/internal/deployer/tencent_teo.go
@@ -0,0 +1,111 @@
+package deployer
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901"
+ "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
+ "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
+ ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
+
+ "github.com/usual2970/certimate/internal/domain"
+ "github.com/usual2970/certimate/internal/utils/rand"
+)
+
+type TencentTEODeployer struct {
+ option *DeployerOption
+ credential *common.Credential
+ infos []string
+}
+
+func NewTencentTEODeployer(option *DeployerOption) (Deployer, error) {
+ access := &domain.TencentAccess{}
+ if err := json.Unmarshal([]byte(option.Access), access); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
+ }
+
+ credential := common.NewCredential(
+ access.SecretId,
+ access.SecretKey,
+ )
+
+ return &TencentTEODeployer{
+ option: option,
+ credential: credential,
+ infos: make([]string, 0),
+ }, nil
+}
+
+func (d *TencentTEODeployer) GetID() string {
+ return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
+}
+
+func (d *TencentTEODeployer) GetInfo() []string {
+ return d.infos
+}
+
+func (d *TencentTEODeployer) Deploy(ctx context.Context) error {
+ // 上传证书
+ certId, err := d.uploadCert()
+ if err != nil {
+ return fmt.Errorf("failed to upload certificate: %w", err)
+ }
+ d.infos = append(d.infos, toStr("上传证书", certId))
+
+ if err := d.deploy(certId); err != nil {
+ return fmt.Errorf("failed to deploy: %w", err)
+ }
+
+ return nil
+}
+
+func (d *TencentTEODeployer) uploadCert() (string, error) {
+ cpf := profile.NewClientProfile()
+ cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
+
+ client, _ := ssl.NewClient(d.credential, "", cpf)
+
+ request := ssl.NewUploadCertificateRequest()
+
+ request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate)
+ request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey)
+ request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6))
+ request.Repeatable = common.BoolPtr(false)
+
+ response, err := client.UploadCertificate(request)
+ if err != nil {
+ return "", fmt.Errorf("failed to upload certificate: %w", err)
+ }
+
+ return *response.Response.CertificateId, nil
+}
+
+func (d *TencentTEODeployer) deploy(certId string) error {
+ cpf := profile.NewClientProfile()
+ cpf.HttpProfile.Endpoint = "teo.tencentcloudapi.com"
+ // 实例化要请求产品的client对象,clientProfile是可选的
+ client, _ := teo.NewClient(d.credential, "", cpf)
+
+ // 实例化一个请求对象,每个接口都会对应一个request对象
+ request := teo.NewModifyHostsCertificateRequest()
+
+ request.ZoneId = common.StringPtr(getDeployString(d.option.DeployConfig, "zoneId"))
+ request.Mode = common.StringPtr("sslcert")
+ request.ServerCertInfo = []*teo.ServerCertInfo{&teo.ServerCertInfo{
+ CertId: common.StringPtr(certId),
+ }}
+
+ domains := strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"),"\n")
+ request.Hosts = common.StringPtrs(domains)
+
+ // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
+ resp, err := client.ModifyHostsCertificate(request)
+ if err != nil {
+ return fmt.Errorf("failed to deploy certificate: %w", err)
+ }
+ d.infos = append(d.infos, toStr("部署证书", resp.Response))
+ return nil
+}
diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx
index 7ef5f291..3d33b870 100644
--- a/ui/src/components/certimate/DeployEditDialog.tsx
+++ b/ui/src/components/certimate/DeployEditDialog.tsx
@@ -14,6 +14,7 @@ import DeployToAliyunCDN from "./DeployToAliyunCDN";
import DeployToTencentCDN from "./DeployToTencentCDN";
import DeployToTencentCLB from "./DeployToTencentCLB";
import DeployToTencentCOS from "./DeployToTencentCOS";
+import DeployToTencentTEO from "./DeployToTencentTEO";
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB";
import DeployToQiniuCDN from "./DeployToQiniuCDN";
@@ -119,6 +120,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
childComponent =