Merge branch 'main' into feat/deployer

This commit is contained in:
Fu Diwei
2024-11-19 09:09:38 +08:00
19 changed files with 1046 additions and 314 deletions

View File

@@ -0,0 +1,116 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
bytepluscdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/byteplus-cdn"
"github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
)
type ByteplusCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *cdn.CDN
sslUploader uploader.Uploader
}
func NewByteplusCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.ByteplusAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client := cdn.NewInstance()
client.Client.SetAccessKey(access.AccessKey)
client.Client.SetSecretKey(access.SecretKey)
uploader, err := bytepluscdn.New(&bytepluscdn.ByteplusCDNUploaderConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &ByteplusCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *ByteplusCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *ByteplusCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *ByteplusCDNDeployer) Deploy(ctx context.Context) error {
apiCtx := context.Background()
// 上传证书
upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
domains := make([]string, 0)
configDomain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(configDomain, "*.") {
// 获取证书可以部署的域名
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-describecertconfig-9ea17
describeCertConfigReq := &cdn.DescribeCertConfigRequest{
CertId: upres.CertId,
}
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
}
for i := range describeCertConfigResp.Result.CertNotConfig {
// 当前未启用 HTTPS 的加速域名列表。
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
}
for i := range describeCertConfigResp.Result.OtherCertConfig {
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联的证书不是您指定的证书。
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
}
for i := range describeCertConfigResp.Result.SpecifiedCertConfig {
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联了您指定的证书。
d.infos = append(d.infos, fmt.Sprintf("%s域名已配置该证书", describeCertConfigResp.Result.SpecifiedCertConfig[i].Domain))
}
if len(domains) == 0 {
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
// 所有匹配的域名都配置了该证书,跳过部署
return nil
} else {
return xerrors.Errorf("未查询到匹配的域名: %s", configDomain)
}
}
} else {
domains = append(domains, configDomain)
}
// 部署证书
// REF: https://github.com/byteplus-sdk/byteplus-sdk-golang/blob/master/service/cdn/api_list.go#L306
for i := range domains {
batchDeployCertReq := &cdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domains[i],
}
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BatchDeployCert'")
} else {
d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), batchDeployCertResp))
}
}
return nil
}

View File

@@ -36,6 +36,7 @@ const (
targetK8sSecret = "k8s-secret"
targetVolcengineLive = "volcengine-live"
targetVolcengineCDN = "volcengine-cdn"
targetByteplusCDN = "byteplus-cdn"
)
type DeployerOption struct {
@@ -150,6 +151,8 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
return NewVolcengineLiveDeployer(option)
case targetVolcengineCDN:
return NewVolcengineCDNDeployer(option)
case targetByteplusCDN:
return NewByteplusCDNDeployer(option)
}
return nil, errors.New("unsupported deploy target")
}

View File

@@ -101,9 +101,9 @@ func (d *TencentCDNDeployer) Deploy(ctx context.Context) error {
}
temp := make([]string, 0)
for _, aliInstanceId := range tcInstanceIds {
if !slices.Contains(deployedDomains, aliInstanceId) {
temp = append(temp, aliInstanceId)
for _, tcInstanceId := range tcInstanceIds {
if !slices.Contains(deployedDomains, tcInstanceId) {
temp = append(temp, tcInstanceId)
}
}
tcInstanceIds = temp

View File

@@ -5,6 +5,11 @@ type AliyunAccess struct {
AccessKeySecret string `json:"accessKeySecret"`
}
type ByteplusAccess struct {
AccessKey string
SecretKey string
}
type TencentAccess struct {
SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"`

View File

@@ -2,14 +2,22 @@ package domains
import (
"context"
"crypto/ecdsa"
"crypto/rsa"
"fmt"
"strings"
"time"
"github.com/pocketbase/pocketbase/models"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/applicant"
"github.com/usual2970/certimate/internal/deployer"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/app"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type Phase string
@@ -20,6 +28,8 @@ const (
deployPhase Phase = "deploy"
)
const validityDuration = time.Hour * 24 * 10
func deploy(ctx context.Context, record *models.Record) error {
defer func() {
if r := recover(); r != nil {
@@ -45,7 +55,10 @@ func deploy(ctx context.Context, record *models.Record) error {
cert := currRecord.GetString("certificate")
expiredAt := currRecord.GetDateTime("expiredAt").Time()
if cert != "" && time.Until(expiredAt) > time.Hour*24*10 && currRecord.GetBool("deployed") {
// 检查证书是否包含设置的所有域名
changed := isCertChanged(cert, currRecord)
if cert != "" && time.Until(expiredAt) > validityDuration && currRecord.GetBool("deployed") && !changed {
app.GetApp().Logger().Info("证书在有效期内")
history.record(checkPhase, "证书在有效期内且已部署,跳过", &RecordInfo{
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
@@ -60,7 +73,7 @@ func deploy(ctx context.Context, record *models.Record) error {
// ############2.申请证书
history.record(applyPhase, "开始申请", nil)
if cert != "" && time.Until(expiredAt) > time.Hour*24 {
if cert != "" && time.Until(expiredAt) > validityDuration && !changed {
history.record(applyPhase, "证书在有效期内,跳过", &RecordInfo{
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
})
@@ -121,3 +134,80 @@ func deploy(ctx context.Context, record *models.Record) error {
return nil
}
func isCertChanged(certificate string, record *models.Record) bool {
// 如果证书为空直接返回true
if certificate == "" {
return true
}
// 解析证书
cert, err := x509.ParseCertificateFromPEM(certificate)
if err != nil {
app.GetApp().Logger().Error("解析证书失败", "err", err)
return true
}
// 遍历域名列表检查是否都在证书中找到第一个不存在证书中域名时提前返回true
for _, domain := range strings.Split(record.GetString("domain"), ";") {
if !slices.Contains(cert.DNSNames, domain) && !slices.Contains(cert.DNSNames, "*."+removeLastSubdomain(domain)) {
return true
}
}
// 解析applyConfig
applyConfig := &domain.ApplyConfig{}
record.UnmarshalJSONField("applyConfig", applyConfig)
// 检查证书加密算法是否变更
switch pubkey := cert.PublicKey.(type) {
case *rsa.PublicKey:
bitSize := pubkey.N.BitLen()
switch bitSize {
case 2048:
// RSA2048
if applyConfig.KeyAlgorithm != "" && applyConfig.KeyAlgorithm != "RSA2048" {
return true
}
case 3072:
// RSA3072
if applyConfig.KeyAlgorithm != "RSA3072" {
return true
}
case 4096:
// RSA4096
if applyConfig.KeyAlgorithm != "RSA4096" {
return true
}
case 8192:
// RSA8192
if applyConfig.KeyAlgorithm != "RSA8192" {
return true
}
}
case *ecdsa.PublicKey:
bitSize := pubkey.Curve.Params().BitSize
switch bitSize {
case 256:
// EC256
if applyConfig.KeyAlgorithm != "EC256" {
return true
}
case 384:
// EC384
if applyConfig.KeyAlgorithm != "EC384" {
return true
}
}
}
return false
}
func removeLastSubdomain(domain string) string {
parts := strings.Split(domain, ".")
if len(parts) > 1 {
return strings.Join(parts[1:], ".")
}
return domain
}

View File

@@ -0,0 +1,111 @@
package bytepluscdn
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
"github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type ByteplusCDNUploaderConfig struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type ByteplusCDNUploader struct {
config *ByteplusCDNUploaderConfig
sdkClient *cdn.CDN
}
var _ uploader.Uploader = (*ByteplusCDNUploader)(nil)
func New(config *ByteplusCDNUploaderConfig) (*ByteplusCDNUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
instance := cdn.NewInstance()
client := instance.Client
client.SetAccessKey(config.AccessKey)
client.SetSecretKey(config.SecretKey)
return &ByteplusCDNUploader{
config: config,
sdkClient: instance,
}, nil
}
func (u *ByteplusCDNUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-listcertinfo
pageNum := int64(1)
pageSize := int64(100)
certSource := "cert_center"
listCertInfoReq := &cdn.ListCertInfoRequest{
PageNum: &pageNum,
PageSize: &pageSize,
Source: &certSource,
}
searchTotal := 0
for {
listCertInfoResp, err := u.sdkClient.ListCertInfo(listCertInfoReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ListCertInfo'")
}
if listCertInfoResp.Result.CertInfo != nil {
for _, certDetail := range listCertInfoResp.Result.CertInfo {
hash := sha256.Sum256(certX509.Raw)
isSameCert := strings.EqualFold(hex.EncodeToString(hash[:]), certDetail.CertFingerprint.Sha256)
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
CertId: certDetail.CertId,
CertName: certDetail.Desc,
}, nil
}
}
}
searchTotal += len(listCertInfoResp.Result.CertInfo)
if int(listCertInfoResp.Result.Total) > searchTotal {
pageNum++
} else {
break
}
}
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-addcertificate
addCertificateReq := &cdn.AddCertificateRequest{
Certificate: certPem,
PrivateKey: privkeyPem,
Source: &certSource,
Desc: &certName,
}
addCertificateResp, err := u.sdkClient.AddCertificate(addCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.AddCertificate'")
}
certId = addCertificateResp.Result.CertId
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}