add tencent cdn deployer

This commit is contained in:
yoan 2024-08-29 11:34:08 +08:00
parent 1861e73531
commit 1e11b23fdc
15 changed files with 288 additions and 128 deletions

View File

@ -15,22 +15,21 @@ Certimate 是一个开源的 SSL 证书管理工具,具有以下特点:
Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决方案。
- [Certimate](#certimate)
- [安装](#安装)
- [二进制文件](#二进制文件)
- [Docker 安装](#docker-安装)
- [默认账号:](#默认账号)
- [概念](#概念)
- [域名](#域名)
- [dns 服务商授权信息](#dns-服务商授权信息)
- [部署服务商授权信息](#部署服务商授权信息)
- [使用](#使用)
- [一、安装](#一安装)
- [1. 二进制文件](#1-二进制文件)
- [2. Docker 安装](#2-docker-安装)
- [二、概念](#二概念)
- [1. 域名](#1-域名)
- [2. dns 服务商授权信息](#2-dns-服务商授权信息)
- [3. 部署服务商授权信息](#3-部署服务商授权信息)
- [三、使用](#三使用)
- [许可证](#许可证)
## 安装
## 一、安装
### 二进制文件
### 1. 二进制文件
你可以直接从[Releases 页](https://github.com/usual2970/certimate/releases)下载预先编译好的二进制文件,解压后执行:
@ -39,7 +38,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
```
### Docker 安装
### 2. Docker 安装
```bash
@ -49,29 +48,27 @@ git clone git@github.com:usual2970/certimate.git && cd certimate/docker && docke
然后在浏览器中访问 http://127.0.0.1:8090 即可访问 Certimate 管理页面。
### 默认账号:
```bash
用户名admin@certimate.fun
密码1234567890
```
## 概念
## 二、概念
Certimate 的工作流程如下:
1. 用户通过 Certimate 管理页面填写申请证书的信息包括域名、dns 服务商的授权信息、以及要部署到的服务商的授权信息。
2. Certimate 向证书场商的 API 发起申请请求,获取 SSL 证书。
3. Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
4. Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
* 用户通过 Certimate 管理页面填写申请证书的信息包括域名、dns 服务商的授权信息、以及要部署到的服务商的授权信息。
* Certimate 向证书场商的 API 发起申请请求,获取 SSL 证书。
* Certimate 存储证书信息,包括证书内容、私钥、证书有效期等,并在证书即将过期时自动续期。
* Certimate 向服务商的 API 发起部署请求,将证书部署到服务商的服务器上。
这就涉及域名、dns 服务商的授权信息、部署服务商的授权信息等。
### 域名
### 1. 域名
就是要申请证书的域名。
### dns 服务商授权信息
### 2. dns 服务商授权信息
给域名申请证书需要证明域名是你的,所以我们手动申请证书的时候一般需要在域名服务商的控制台解析记录中添加一个 TXT 记录。
@ -91,13 +88,13 @@ secretId: xxx
secretKey: TOKEN
```
### 部署服务商授权信息
### 3. 部署服务商授权信息
Certimate 申请证书后,会自动将证书部署到你指定的目标上,比如阿里云 CDN 这时你需要填写阿里云的授权信息。Certimate 会根据你填写的授权信息及域名找到对应的 CDN 服务,并将证书部署到对应的 CDN 服务上。
部署服务商授权信息和 dns 服务商授权信息一致,区别在于 dns 服务商授权信息用于证明域名是你的,部署服务商授权信息用于提供证书部署的授权信息。
## 使用
## 三、使用
![Alt text](usage.gif)

4
go.mod
View File

@ -16,6 +16,9 @@ require (
github.com/pkg/sftp v1.13.6
github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/pocketbase v0.22.18
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.992
golang.org/x/crypto v0.26.0
)
@ -90,7 +93,6 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 // indirect
github.com/tjfoc/gmsm v1.3.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect

7
go.sum
View File

@ -300,10 +300,15 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 h1:ERwcXqhc94L9cFxtiI0pvt7IJtlHl/p/Jayl3mLw+ms=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992 h1:266lOve+E8vzhnrb/Mr05Ee+oxXD9C82JiusY/AZqXw=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 h1:LoYv5u+gUoFpU/AmIuTRG/2KiEkdm9gCC0dTvk8WITQ=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898/go.mod h1:c1j6YQ+vCbeA8kJ59Im4UnMd1GxovlpPBDhGZoewfn8=
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/tag v1.0.992 h1:ttCM2rrkGipHMFTavrPExKCWcfNjT7AMQ5ERrPExdI4=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.992/go.mod h1:WtzarrflM+eoyD8vcRuIPd8fT5UXD4IhUry6iSAUnxc=
github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=

View File

@ -1,17 +1,13 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"os"
"github.com/go-acme/lego/v4/providers/dns/alidns"
)
type aliyunAccess struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type aliyun struct {
option *ApplyOption
}
@ -24,7 +20,7 @@ func NewAliyun(option *ApplyOption) Applicant {
func (a *aliyun) Apply() (*Certificate, error) {
access := &aliyunAccess{}
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("ALICLOUD_ACCESS_KEY", access.AccessKeyId)

View File

@ -1,17 +1,13 @@
package applicant
import (
"certimate/internal/domain"
"encoding/json"
"os"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
)
type tencentAccess struct {
SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"`
}
type tencent struct {
option *ApplyOption
}
@ -24,7 +20,7 @@ func NewTencent(option *ApplyOption) Applicant {
func (t *tencent) Apply() (*Certificate, error) {
access := &tencentAccess{}
access := &domain.TencentAccess{}
json.Unmarshal([]byte(t.option.Access), access)
os.Setenv("TENCENTCLOUD_SECRET_ID", access.SecretId)

View File

@ -2,6 +2,7 @@ package deployer
import (
"certimate/internal/applicant"
"certimate/internal/domain"
"certimate/internal/utils/rand"
"context"
"encoding/json"
@ -15,18 +16,13 @@ import (
"github.com/alibabacloud-go/tea/tea"
)
type aliyunAccess struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type aliyun struct {
client *cas20200407.Client
option *DeployerOption
}
func NewAliyun(option *DeployerOption) (Deployer, error) {
access := &aliyunAccess{}
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
a := &aliyun{
option: option,

View File

@ -1,6 +1,7 @@
package deployer
import (
"certimate/internal/domain"
"context"
"encoding/json"
"fmt"
@ -17,7 +18,7 @@ type AliyunCdn struct {
}
func NewAliyunCdn(option *DeployerOption) (*AliyunCdn, error) {
access := &aliyunAccess{}
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
a := &AliyunCdn{
option: option,

View File

@ -18,6 +18,7 @@ const (
targetAliyunCdn = "aliyun-cdn"
targetSSH = "ssh"
targetWebhook = "webhook"
targetTencentCdn = "tencent-cdn"
)
type DeployerOption struct {
@ -58,6 +59,8 @@ func Get(record *models.Record, cert *applicant.Certificate) (Deployer, error) {
return NewSSH(option)
case targetWebhook:
return NewWebhook(option)
case targetTencentCdn:
return NewTencentCdn(option)
}
return nil, errors.New("not implemented")
}

View File

@ -0,0 +1,161 @@
package deployer
import (
"certimate/internal/domain"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"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"
tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813"
)
type tencentCdn struct {
option *DeployerOption
credential *common.Credential
}
func NewTencentCdn(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 &tencentCdn{
option: option,
credential: credential,
}, nil
}
func (t *tencentCdn) Deploy(ctx context.Context) error {
// 查询有没有对应的资源
resource, err := t.resource()
if err != nil {
return fmt.Errorf("failed to get resource: %w", err)
}
// 上传证书
certId, err := t.uploadCert()
if err != nil {
return fmt.Errorf("failed to upload certificate: %w", err)
}
if err := t.deploy(resource, certId); err != nil {
return fmt.Errorf("failed to deploy: %w", err)
}
return nil
}
func (t *tencentCdn) uploadCert() (string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
client, _ := ssl.NewClient(t.credential, "", cpf)
request := ssl.NewUploadCertificateRequest()
request.CertificatePublicKey = common.StringPtr(t.option.Certificate.Certificate)
request.CertificatePrivateKey = common.StringPtr(t.option.Certificate.PrivateKey)
request.Alias = common.StringPtr(t.option.Domain)
request.Repeatable = common.BoolPtr(true)
response, err := client.UploadCertificate(request)
if err != nil {
return "", fmt.Errorf("failed to upload certificate: %w", err)
}
return *response.Response.CertificateId, nil
}
func (t *tencentCdn) deploy(resource *tag.ResourceTagMapping, certId string) error {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := ssl.NewClient(t.credential, "", cpf)
resourceId, err := getResourceId(resource)
if err != nil {
return fmt.Errorf("failed to get resource id: %w", err)
}
// 实例化一个请求对象,每个接口都会对应一个request对象
request := ssl.NewDeployCertificateInstanceRequest()
request.CertificateId = common.StringPtr(certId)
request.InstanceIdList = common.StringPtrs([]string{resourceId})
request.ResourceType = common.StringPtr("cdn")
request.Status = common.Int64Ptr(1)
// 返回的resp是一个DeployCertificateInstanceResponse的实例与请求对象对应
_, err = client.DeployCertificateInstance(request)
if err != nil {
return fmt.Errorf("failed to deploy certificate: %w", err)
}
return nil
}
func (t *tencentCdn) resource() (*tag.ResourceTagMapping, error) {
request := tag.NewGetResourcesRequest()
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "tag.tencentcloudapi.com"
client, err := tag.NewClient(t.credential, "", cpf)
if err != nil {
return nil, fmt.Errorf("failed to create client: %w", err)
}
response, err := client.GetResources(request)
if err != nil {
return nil, fmt.Errorf("failed to get resources: %w", err)
}
for _, resource := range response.Response.ResourceTagMappingList {
if t.compare(resource) {
return resource, nil
}
}
return nil, errors.New("no resource found")
}
func (t *tencentCdn) compare(resource *tag.ResourceTagMapping) bool {
slices := strings.Split(*resource.Resource, "/")
if len(slices) != 3 {
return false
}
typeSlices := strings.Split(slices[0], "::")
if len(typeSlices) != 3 {
return false
}
if typeSlices[1] != "cdn" || slices[2] != t.option.Domain {
return false
}
return true
}
func getResourceId(resource *tag.ResourceTagMapping) (string, error) {
slices := strings.Split(*resource.Resource, "/")
if len(slices) != 3 {
return "", errors.New("invalid resource")
}
return slices[2], nil
}

13
internal/domain/access.go Normal file
View File

@ -0,0 +1,13 @@
package domain
type AliyunAccess struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type TencentAccess struct {
SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"`
}

File diff suppressed because one or more lines are too long

2
ui/dist/index.html vendored
View File

@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Certimate - Your Trusted SSL Automation Partner</title>
<script type="module" crossorigin src="/assets/index-D-MCicNE.js"></script>
<script type="module" crossorigin src="/assets/index-CGKfLY3n.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-VYJgHfoP.css">
</head>
<body class="bg-background">

View File

@ -30,6 +30,7 @@ export const getLastDeployment = (domain: Domain): Deployment | undefined => {
export const targetTypeMap: Map<string, [string, string]> = new Map([
["aliyun-cdn", ["阿里云-CDN", "/imgs/providers/aliyun.svg"]],
["aliyun-oss", ["阿里云-OSS", "/imgs/providers/aliyun.svg"]],
["tencent-cdn", ["腾讯云-CDN", "/imgs/providers/tencent.svg"]],
["ssh", ["SSH部署", "/imgs/providers/ssh.png"]],
["webhook", ["Webhook", "/imgs/providers/webhook.svg"]],
]);

View File

@ -187,7 +187,7 @@ export default function Dashboard() {
href="https://github.com/usual2970/certimate/releases"
target="_blank"
>
Certimate v0.0.5
Certimate v0.0.6
</a>
</div>
</div>

View File

@ -34,9 +34,6 @@ import { useLocation, useNavigate } from "react-router-dom";
import { Plus } from "lucide-react";
import { AccessEdit } from "@/components/certimate/AccessEdit";
import { accessTypeMap } from "@/domain/access";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
const Edit = () => {
const {
@ -151,10 +148,6 @@ const Edit = () => {
}
};
const getOptionCls = (val: string) => {
return form.getValues().targetType == val ? "border-primary" : "";
};
return (
<>
<div className="">
@ -241,37 +234,33 @@ const Edit = () => {
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<RadioGroup
className="flex mt-3 space-x-2"
onValueChange={(val: string) => {
setTargetType(val);
form.setValue("targetType", val);
}}
<Select
{...field}
onValueChange={(value) => {
setTargetType(value);
form.setValue("targetType", value);
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择部署服务类型" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{targetTypeKeys.map((key) => (
<div
className="flex items-center space-x-2"
key={key}
>
<Label>
<RadioGroupItem value={key} id={key} hidden />
<div
className={cn(
"flex items-center space-x-2 border p-2 rounded cursor-pointer",
getOptionCls(key)
)}
>
<SelectItem key={key} value={key}>
<div className="flex items-center space-x-2">
<img
className="w-6"
src={targetTypeMap.get(key)?.[1]}
className="h-6"
/>
<div>{targetTypeMap.get(key)?.[0]}</div>
</div>
</Label>
</div>
</SelectItem>
))}
</RadioGroup>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />