From 9febe479755fca88237980a72cd7b88a7b2ca793 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 20 Feb 2025 14:32:42 +0800 Subject: [PATCH] feat: add jdcloud ssl uploader --- .../providers/jdcloud-cdn/jdcloud_cdn.go | 36 ++++- .../providers/jdcloud-ssl/jdcloud_ssl.go | 138 ++++++++++++++++++ .../providers/jdcloud-ssl/jdcloud_ssl_test.go | 72 +++++++++ 3 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go create mode 100644 internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl_test.go diff --git a/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go b/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go index 9b29d9c1..17523d1c 100644 --- a/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go +++ b/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go @@ -10,6 +10,8 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/logger" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/jdcloud-ssl" ) type DeployerConfig struct { @@ -22,9 +24,10 @@ type DeployerConfig struct { } type DeployerProvider struct { - config *DeployerConfig - logger logger.Logger - sdkClient *jdCdnClient.CdnClient + config *DeployerConfig + logger logger.Logger + sdkClient *jdCdnClient.CdnClient + sslUploader uploader.Uploader } var _ deployer.Deployer = (*DeployerProvider)(nil) @@ -39,10 +42,19 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, xerrors.Wrap(err, "failed to create sdk client") } + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: config.AccessKeyId, + AccessKeySecret: config.AccessKeySecret, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } + return &DeployerProvider{ - config: config, - logger: logger.NewNilLogger(), - sdkClient: client, + config: config, + logger: logger.NewNilLogger(), + sdkClient: client, + sslUploader: uploader, }, nil } @@ -62,14 +74,22 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe d.logger.Logt("已查询到域名配置信息", queryDomainConfigResp) } + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem) + if err != nil { + return nil, xerrors.Wrap(err, "failed to upload certificate file") + } else { + d.logger.Logt("certificate file uploaded", upres) + } + // 设置通讯协议 // REF: https://docs.jdcloud.com/cn/cdn/api/sethttptype setHttpTypeReq := jdCdnApi.NewSetHttpTypeRequest(d.config.Domain) setHttpTypeReq.SetHttpType("https") - setHttpTypeReq.SetCertFrom("default") setHttpTypeReq.SetCertificate(certPem) setHttpTypeReq.SetRsaKey(privkeyPem) - setHttpTypeReq.SetSyncToSsl(false) + setHttpTypeReq.SetCertFrom("ssl") + setHttpTypeReq.SetSslCertId(upres.CertId) setHttpTypeReq.SetJumpType(queryDomainConfigResp.Result.HttpsJumpType) setHttpTypeResp, err := d.sdkClient.SetHttpType(setHttpTypeReq) if err != nil { diff --git a/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go b/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go new file mode 100644 index 00000000..6368bc51 --- /dev/null +++ b/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go @@ -0,0 +1,138 @@ +package jdcloudssl + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + "time" + + jdCore "github.com/jdcloud-api/jdcloud-sdk-go/core" + jdSslApi "github.com/jdcloud-api/jdcloud-sdk-go/services/ssl/apis" + jdSslClient "github.com/jdcloud-api/jdcloud-sdk-go/services/ssl/client" + xerrors "github.com/pkg/errors" + "golang.org/x/exp/slices" + + "github.com/usual2970/certimate/internal/pkg/core/uploader" + "github.com/usual2970/certimate/internal/pkg/utils/certs" +) + +type UploaderConfig struct { + // 京东云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 京东云 AccessKeySecret。 + AccessKeySecret string `json:"accessKeySecret"` +} + +type UploaderProvider struct { + config *UploaderConfig + sdkClient *jdSslClient.SslClient +} + +var _ uploader.Uploader = (*UploaderProvider)(nil) + +func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + return &UploaderProvider{ + config: config, + sdkClient: client, + }, nil +} + +func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { + // 解析证书内容 + certX509, err := certs.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 格式化私钥内容,以便后续计算私钥摘要 + privkeyPem = strings.TrimSpace(privkeyPem) + privkeyPem = strings.ReplaceAll(privkeyPem, "\r", "") + privkeyPem = strings.ReplaceAll(privkeyPem, "\n", "\r\n") + privkeyPem = privkeyPem + "\r\n" + + // 遍历查看证书列表,避免重复上传 + // REF: https://docs.jdcloud.com/cn/ssl-certificate/api/describecerts + describeCertsPageNumber := 1 + describeCertsPageSize := 100 + for { + describeCertsReq := jdSslApi.NewDescribeCertsRequest() + describeCertsReq.DomainName = &certX509.Subject.CommonName + describeCertsReq.PageNumber = &describeCertsPageNumber + describeCertsReq.PageSize = &describeCertsPageSize + describeCertsResp, err := u.sdkClient.DescribeCerts(describeCertsReq) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.DescribeCerts'") + } + + for _, certDetail := range describeCertsResp.Result.CertListDetails { + // 先尝试匹配 CN + if !strings.EqualFold(certX509.Subject.CommonName, certDetail.CommonName) { + continue + } + + // 再尝试匹配 SAN + if !slices.Equal(certX509.DNSNames, certDetail.DnsNames) { + continue + } + + // 再尝试匹配证书有效期 + oldCertNotBefore, _ := time.Parse(time.RFC3339, certDetail.StartTime) + oldCertNotAfter, _ := time.Parse(time.RFC3339, certDetail.EndTime) + if !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) { + continue + } + + // 最后尝试匹配私钥摘要 + newKeyDigest := sha256.Sum256([]byte(privkeyPem)) + newKeyDigestHex := hex.EncodeToString(newKeyDigest[:]) + if !strings.EqualFold(newKeyDigestHex, certDetail.Digest) { + continue + } + + // 如果以上都匹配,则视为已存在相同证书,直接返回已有的证书信息 + return &uploader.UploadResult{ + CertId: certDetail.CertId, + CertName: certDetail.CertName, + }, nil + } + + if len(describeCertsResp.Result.CertListDetails) < int(describeCertsPageSize) { + break + } else { + describeCertsPageNumber++ + } + } + + // 生成新证书名(需符合京东云命名规则) + certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + + // 上传证书 + // REF: https://docs.jdcloud.com/cn/ssl-certificate/api/uploadcert + uploadCertReq := jdSslApi.NewUploadCertRequest(certName, privkeyPem, certPem) + uploadCertResp, err := u.sdkClient.UploadCert(uploadCertReq) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.UploadCertificate'") + } + + return &uploader.UploadResult{ + CertId: uploadCertResp.Result.CertId, + CertName: certName, + }, nil +} + +func createSdkClient(accessKeyId, accessKeySecret string) (*jdSslClient.SslClient, error) { + clientCredentials := jdCore.NewCredentials(accessKeyId, accessKeySecret) + client := jdSslClient.NewSslClient(clientCredentials) + return client, nil +} diff --git a/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl_test.go b/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl_test.go new file mode 100644 index 00000000..ec02ce49 --- /dev/null +++ b/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl_test.go @@ -0,0 +1,72 @@ +package jdcloudssl_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/jdcloud-ssl" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fAccessKeySecret string +) + +func init() { + argsPrefix := "CERTIMATE_UPLOADER_JDCLOUDSSL_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") +} + +/* +Shell command to run this test: + + go test -v ./jdcloud_ssl_test.go -args \ + --CERTIMATE_UPLOADER_JDCLOUDSSL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_UPLOADER_JDCLOUDSSL_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_UPLOADER_JDCLOUDSSL_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_UPLOADER_JDCLOUDSSL_ACCESSKEYSECRET="your-access-key-secret" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), + }, "\n")) + + uploader, err := provider.NewUploader(&provider.UploaderConfig{ + AccessKeyId: fAccessKeyId, + AccessKeySecret: fAccessKeySecret, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + sres, _ := json.Marshal(res) + t.Logf("ok: %s", string(sres)) + }) +}