refactor: modify directory structure

This commit is contained in:
Fu Diwei
2025-06-17 14:01:34 +08:00
parent 299a722aa9
commit 30840bbba5
346 changed files with 5051 additions and 5086 deletions

View File

@@ -0,0 +1,136 @@
package onepanelconsole
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"strconv"
"github.com/usual2970/certimate/internal/pkg/core"
onepanelsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/1panel"
onepanelsdkv2 "github.com/usual2970/certimate/internal/pkg/sdk3rd/1panel/v2"
)
type SSLDeployerProviderConfig struct {
// 1Panel 服务地址。
ServerUrl string `json:"serverUrl"`
// 1Panel 版本。
// 可取值 "v1"、"v2"。
ApiVersion string `json:"apiVersion"`
// 1Panel 接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 是否自动重启。
AutoRestart bool `json:"autoRestart"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient any
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 设置面板 SSL 证书
switch sdkClient := d.sdkClient.(type) {
case *onepanelsdk.Client:
{
updateSettingsSSLReq := &onepanelsdk.UpdateSettingsSSLRequest{
Cert: certPEM,
Key: privkeyPEM,
SSL: "enable",
SSLType: "import-paste",
AutoRestart: strconv.FormatBool(d.config.AutoRestart),
}
updateSystemSSLResp, err := sdkClient.UpdateSettingsSSL(updateSettingsSSLReq)
d.logger.Debug("sdk request '1panel.UpdateSettingsSSL'", slog.Any("request", updateSettingsSSLReq), slog.Any("response", updateSystemSSLResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request '1panel.UpdateSettingsSSL': %w", err)
}
}
case *onepanelsdkv2.Client:
{
updateCoreSettingsSSLReq := &onepanelsdkv2.UpdateCoreSettingsSSLRequest{
Cert: certPEM,
Key: privkeyPEM,
SSL: "Enable",
SSLType: "import-paste",
AutoRestart: strconv.FormatBool(d.config.AutoRestart),
}
updateCoreSystemSSLResp, err := sdkClient.UpdateCoreSettingsSSL(updateCoreSettingsSSLReq)
d.logger.Debug("sdk request '1panel.UpdateCoreSettingsSSL'", slog.Any("request", updateCoreSettingsSSLReq), slog.Any("response", updateCoreSystemSSLResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request '1panel.UpdateCoreSettingsSSL': %w", err)
}
}
default:
panic("sdk client is not implemented")
}
return &core.SSLDeployResult{}, nil
}
const (
sdkVersionV1 = "v1"
sdkVersionV2 = "v2"
)
func createSDKClient(serverUrl, apiVersion, apiKey string, skipTlsVerify bool) (any, error) {
if apiVersion == sdkVersionV1 {
client, err := onepanelsdk.NewClient(serverUrl, apiKey)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
} else if apiVersion == sdkVersionV2 {
client, err := onepanelsdkv2.NewClient(serverUrl, apiKey)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}
return nil, fmt.Errorf("invalid 1panel api version")
}

View File

@@ -0,0 +1,77 @@
package onepanelconsole_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/1panel-console"
)
var (
fInputCertPath string
fInputKeyPath string
fServerUrl string
fApiVersion string
fApiKey string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_1PANELCONSOLE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fApiVersion, argsPrefix+"APIVERSION", "v1", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./1panel_console_test.go -args \
--CERTIMATE_SSLDEPLOYER_1PANELCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_1PANELCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_1PANELCONSOLE_SERVERURL="http://127.0.0.1:20410" \
--CERTIMATE_SSLDEPLOYER_1PANELCONSOLE_APIVERSION="v1" \
--CERTIMATE_SSLDEPLOYER_1PANELCONSOLE_APIKEY="your-api-key"
*/
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("SERVERURL: %v", fServerUrl),
fmt.Sprintf("APIVERSION: %v", fApiVersion),
fmt.Sprintf("APIKEY: %v", fApiKey),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ServerUrl: fServerUrl,
ApiVersion: fApiVersion,
ApiKey: fApiKey,
AllowInsecureConnections: true,
AutoRestart: true,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,273 @@
package onepanelsite
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"strconv"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/1panel-ssl"
onepanelsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/1panel"
onepanelsdkv2 "github.com/usual2970/certimate/internal/pkg/sdk3rd/1panel/v2"
)
type SSLDeployerProviderConfig struct {
// 1Panel 服务地址。
ServerUrl string `json:"serverUrl"`
// 1Panel 版本。
// 可取值 "v1"、"v2"。
ApiVersion string `json:"apiVersion"`
// 1Panel 接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 网站 ID。
// 部署资源类型为 [RESOURCE_TYPE_WEBSITE] 时必填。
WebsiteId int64 `json:"websiteId,omitempty"`
// 证书 ID。
// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。
CertificateId int64 `json:"certificateId,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient any
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
ServerUrl: config.ServerUrl,
ApiVersion: config.ApiVersion,
ApiKey: config.ApiKey,
AllowInsecureConnections: config.AllowInsecureConnections,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_WEBSITE:
if err := d.deployToWebsite(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
case RESOURCE_TYPE_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToWebsite(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.WebsiteId == 0 {
return errors.New("config `websiteId` is required")
}
// 上传证书
upres, err := d.sslManager.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
switch sdkClient := d.sdkClient.(type) {
case *onepanelsdk.Client:
{
// 获取网站 HTTPS 配置
getHttpsConfResp, err := sdkClient.GetHttpsConf(d.config.WebsiteId)
d.logger.Debug("sdk request '1panel.GetHttpsConf'", slog.Int64("websiteId", d.config.WebsiteId), slog.Any("response", getHttpsConfResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.GetHttpsConf': %w", err)
}
// 修改网站 HTTPS 配置
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
updateHttpsConfReq := &onepanelsdk.UpdateHttpsConfRequest{
WebsiteID: d.config.WebsiteId,
Type: "existed",
WebsiteSSLID: certId,
Enable: getHttpsConfResp.Data.Enable,
HttpConfig: getHttpsConfResp.Data.HttpConfig,
SSLProtocol: getHttpsConfResp.Data.SSLProtocol,
Algorithm: getHttpsConfResp.Data.Algorithm,
Hsts: getHttpsConfResp.Data.Hsts,
}
updateHttpsConfResp, err := sdkClient.UpdateHttpsConf(d.config.WebsiteId, updateHttpsConfReq)
d.logger.Debug("sdk request '1panel.UpdateHttpsConf'", slog.Int64("websiteId", d.config.WebsiteId), slog.Any("request", updateHttpsConfReq), slog.Any("response", updateHttpsConfResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.UpdateHttpsConf': %w", err)
}
}
case *onepanelsdkv2.Client:
{
// 获取网站 HTTPS 配置
getHttpsConfResp, err := sdkClient.GetHttpsConf(d.config.WebsiteId)
d.logger.Debug("sdk request '1panel.GetHttpsConf'", slog.Int64("websiteId", d.config.WebsiteId), slog.Any("response", getHttpsConfResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.GetHttpsConf': %w", err)
}
// 修改网站 HTTPS 配置
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
updateHttpsConfReq := &onepanelsdkv2.UpdateHttpsConfRequest{
WebsiteID: d.config.WebsiteId,
Type: "existed",
WebsiteSSLID: certId,
Enable: getHttpsConfResp.Data.Enable,
HttpConfig: getHttpsConfResp.Data.HttpConfig,
SSLProtocol: getHttpsConfResp.Data.SSLProtocol,
Algorithm: getHttpsConfResp.Data.Algorithm,
Hsts: getHttpsConfResp.Data.Hsts,
}
updateHttpsConfResp, err := sdkClient.UpdateHttpsConf(d.config.WebsiteId, updateHttpsConfReq)
d.logger.Debug("sdk request '1panel.UpdateHttpsConf'", slog.Int64("websiteId", d.config.WebsiteId), slog.Any("request", updateHttpsConfReq), slog.Any("response", updateHttpsConfResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.UpdateHttpsConf': %w", err)
}
}
default:
panic("sdk client is not implemented")
}
return nil
}
func (d *SSLDeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.CertificateId == 0 {
return errors.New("config `certificateId` is required")
}
switch sdkClient := d.sdkClient.(type) {
case *onepanelsdk.Client:
{
// 获取证书详情
getWebsiteSSLResp, err := sdkClient.GetWebsiteSSL(d.config.CertificateId)
d.logger.Debug("sdk request '1panel.GetWebsiteSSL'", slog.Any("sslId", d.config.CertificateId), slog.Any("response", getWebsiteSSLResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.GetWebsiteSSL': %w", err)
}
// 更新证书
uploadWebsiteSSLReq := &onepanelsdk.UploadWebsiteSSLRequest{
SSLID: d.config.CertificateId,
Type: "paste",
Description: getWebsiteSSLResp.Data.Description,
Certificate: certPEM,
PrivateKey: privkeyPEM,
}
uploadWebsiteSSLResp, err := sdkClient.UploadWebsiteSSL(uploadWebsiteSSLReq)
d.logger.Debug("sdk request '1panel.UploadWebsiteSSL'", slog.Any("request", uploadWebsiteSSLReq), slog.Any("response", uploadWebsiteSSLResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.UploadWebsiteSSL': %w", err)
}
}
case *onepanelsdkv2.Client:
{
// 获取证书详情
getWebsiteSSLResp, err := sdkClient.GetWebsiteSSL(d.config.CertificateId)
d.logger.Debug("sdk request '1panel.GetWebsiteSSL'", slog.Any("sslId", d.config.CertificateId), slog.Any("response", getWebsiteSSLResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.GetWebsiteSSL': %w", err)
}
// 更新证书
uploadWebsiteSSLReq := &onepanelsdkv2.UploadWebsiteSSLRequest{
SSLID: d.config.CertificateId,
Type: "paste",
Description: getWebsiteSSLResp.Data.Description,
Certificate: certPEM,
PrivateKey: privkeyPEM,
}
uploadWebsiteSSLResp, err := sdkClient.UploadWebsiteSSL(uploadWebsiteSSLReq)
d.logger.Debug("sdk request '1panel.UploadWebsiteSSL'", slog.Any("request", uploadWebsiteSSLReq), slog.Any("response", uploadWebsiteSSLResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.UploadWebsiteSSL': %w", err)
}
}
default:
panic("sdk client is not implemented")
}
return nil
}
const (
sdkVersionV1 = "v1"
sdkVersionV2 = "v2"
)
func createSDKClient(serverUrl, apiVersion, apiKey string, skipTlsVerify bool) (any, error) {
if apiVersion == sdkVersionV1 {
client, err := onepanelsdk.NewClient(serverUrl, apiKey)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
} else if apiVersion == sdkVersionV2 {
client, err := onepanelsdkv2.NewClient(serverUrl, apiKey)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}
return nil, fmt.Errorf("invalid 1panel api version")
}

View File

@@ -0,0 +1,82 @@
package onepanelsite_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/1panel-site"
)
var (
fInputCertPath string
fInputKeyPath string
fServerUrl string
fApiVersion string
fApiKey string
fWebsiteId int64
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_1PANELSITE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fApiVersion, argsPrefix+"APIVERSION", "v1", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.Int64Var(&fWebsiteId, argsPrefix+"WEBSITEID", 0, "")
}
/*
Shell command to run this test:
go test -v ./1panel_site_test.go -args \
--CERTIMATE_SSLDEPLOYER_1PANELSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_1PANELSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_1PANELSITE_SERVERURL="http://127.0.0.1:20410" \
--CERTIMATE_SSLDEPLOYER_1PANELSITE_APIVERSION="v1" \
--CERTIMATE_SSLDEPLOYER_1PANELSITE_APIKEY="your-api-key" \
--CERTIMATE_SSLDEPLOYER_1PANELSITE_WEBSITEID="your-website-id"
*/
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("SERVERURL: %v", fServerUrl),
fmt.Sprintf("APIVERSION: %v", fApiVersion),
fmt.Sprintf("APIKEY: %v", fApiKey),
fmt.Sprintf("WEBSITEID: %v", fWebsiteId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ServerUrl: fServerUrl,
ApiVersion: fApiVersion,
ApiKey: fApiKey,
AllowInsecureConnections: true,
ResourceType: provider.RESOURCE_TYPE_WEBSITE,
WebsiteId: fWebsiteId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,10 @@
package onepanelsite
type ResourceType string
const (
// 资源类型:替换指定网站的证书。
RESOURCE_TYPE_WEBSITE = ResourceType("website")
// 资源类型:替换指定证书。
RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate")
)

View File

@@ -0,0 +1,487 @@
package aliyunalb
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
"time"
alialb "github.com/alibabacloud-go/alb-20200616/v2/client"
alicas "github.com/alibabacloud-go/cas-20200407/v3/client"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aliyun-cas"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听 ID。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
// SNI 域名(支持泛域名)。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。
Domain string `json:"domain,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClients *wSDKClients
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
type wSDKClients struct {
ALB *alialb.Client
CAS *alicas.Client
}
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
clients, err := createSDKClients(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := createSSLManager(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClients: clients,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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))
}
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case RESOURCE_TYPE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute
getLoadBalancerAttributeReq := &alialb.GetLoadBalancerAttributeRequest{
LoadBalancerId: tea.String(d.config.LoadbalancerId),
}
getLoadBalancerAttributeResp, err := d.sdkClients.ALB.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
d.logger.Debug("sdk request 'alb.GetLoadBalancerAttribute'", slog.Any("request", getLoadBalancerAttributeReq), slog.Any("response", getLoadBalancerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.GetLoadBalancerAttribute': %w", err)
}
// 查询 HTTPS 监听列表
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
listenerIds := make([]string, 0)
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenersReq := &alialb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
ListenerProtocol: tea.String("HTTPS"),
}
listListenersResp, err := d.sdkClients.ALB.ListListeners(listListenersReq)
d.logger.Debug("sdk request 'alb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err)
}
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))
}
}
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = listListenersResp.Body.NextToken
}
}
// 查询 QUIC 监听列表
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
listListenersToken = nil
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenersReq := &alialb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
ListenerProtocol: tea.String("QUIC"),
}
listListenersResp, err := d.sdkClients.ALB.ListListeners(listListenersReq)
d.logger.Debug("sdk request 'alb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err)
}
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))
}
}
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = listListenersResp.Body.NextToken
}
}
// 遍历更新监听证书
if len(listenerIds) == 0 {
d.logger.Info("no alb listeners to deploy")
} else {
var errs []error
d.logger.Info("found https/quic listeners to deploy", slog.Any("listenerIds", listenerIds))
for _, listenerId := range listenerIds {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *SSLDeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 更新监听
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
return err
}
return nil
}
func (d *SSLDeployerProvider) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
// 查询监听的属性
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute
getListenerAttributeReq := &alialb.GetListenerAttributeRequest{
ListenerId: tea.String(cloudListenerId),
}
getListenerAttributeResp, err := d.sdkClients.ALB.GetListenerAttribute(getListenerAttributeReq)
d.logger.Debug("sdk request 'alb.GetListenerAttribute'", slog.Any("request", getListenerAttributeReq), slog.Any("response", getListenerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.GetListenerAttribute': %w", err)
}
if d.config.Domain == "" {
// 未指定 SNI只需部署到监听器
// 修改监听的属性
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute
updateListenerAttributeReq := &alialb.UpdateListenerAttributeRequest{
ListenerId: tea.String(cloudListenerId),
Certificates: []*alialb.UpdateListenerAttributeRequestCertificates{{
CertificateId: tea.String(cloudCertId),
}},
}
updateListenerAttributeResp, err := d.sdkClients.ALB.UpdateListenerAttribute(updateListenerAttributeReq)
d.logger.Debug("sdk request 'alb.UpdateListenerAttribute'", slog.Any("request", updateListenerAttributeReq), slog.Any("response", updateListenerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.UpdateListenerAttribute': %w", err)
}
} else {
// 指定 SNI需部署到扩展域名
// 查询监听证书列表
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlistenercertificates
listenerCertificates := make([]alialb.ListListenerCertificatesResponseBodyCertificates, 0)
listListenerCertificatesLimit := int32(100)
var listListenerCertificatesToken *string = nil
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenerCertificatesReq := &alialb.ListListenerCertificatesRequest{
NextToken: listListenerCertificatesToken,
MaxResults: tea.Int32(listListenerCertificatesLimit),
ListenerId: tea.String(cloudListenerId),
CertificateType: tea.String("Server"),
}
listListenerCertificatesResp, err := d.sdkClients.ALB.ListListenerCertificates(listListenerCertificatesReq)
d.logger.Debug("sdk request 'alb.ListListenerCertificates'", slog.Any("request", listListenerCertificatesReq), slog.Any("response", listListenerCertificatesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.ListListenerCertificates': %w", err)
}
if listListenerCertificatesResp.Body.Certificates != nil {
for _, listenerCertificate := range listListenerCertificatesResp.Body.Certificates {
listenerCertificates = append(listenerCertificates, *listenerCertificate)
}
}
if len(listListenerCertificatesResp.Body.Certificates) == 0 || listListenerCertificatesResp.Body.NextToken == nil {
break
} else {
listListenerCertificatesToken = listListenerCertificatesResp.Body.NextToken
}
}
// 遍历查询监听证书,并找出需要解除关联的证书
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlistenercertificates
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getusercertificatedetail
certificateIsAlreadyAssociated := false
certificateIdsToDissociate := make([]string, 0)
if len(listenerCertificates) > 0 {
d.logger.Info("found listener certificates to deploy", slog.Any("listenerCertificates", listenerCertificates))
var errs []error
for _, listenerCertificate := range listenerCertificates {
if tea.BoolValue(listenerCertificate.IsDefault) {
continue
}
if !strings.EqualFold(tea.StringValue(listenerCertificate.Status), "Associated") {
continue
}
// 监听证书 ID 格式:${证书 ID}-${地域}
certificateId := strings.Split(tea.StringValue(listenerCertificate.CertificateId), "-")[0]
if certificateId == cloudCertId {
certificateIsAlreadyAssociated = true
break
}
certificateIdAsInt64, err := strconv.ParseInt(certificateId, 10, 64)
if err != nil {
errs = append(errs, err)
continue
}
getUserCertificateDetailReq := &alicas.GetUserCertificateDetailRequest{
CertId: tea.Int64(certificateIdAsInt64),
}
getUserCertificateDetailResp, err := d.sdkClients.CAS.GetUserCertificateDetail(getUserCertificateDetailReq)
d.logger.Debug("sdk request 'cas.GetUserCertificateDetail'", slog.Any("request", getUserCertificateDetailReq), slog.Any("response", getUserCertificateDetailResp))
if err != nil {
if sdkerr, ok := err.(*tea.SDKError); ok {
if tea.IntValue(sdkerr.StatusCode) == 400 && tea.StringValue(sdkerr.Code) == "NotFound" {
continue
}
}
errs = append(errs, fmt.Errorf("failed to execute sdk request 'cas.GetUserCertificateDetail': %w", err))
continue
} else {
certCNMatched := tea.StringValue(getUserCertificateDetailResp.Body.Common) == d.config.Domain
certSANMatched := slices.Contains(strings.Split(tea.StringValue(getUserCertificateDetailResp.Body.Sans), ","), d.config.Domain)
if !certCNMatched && !certSANMatched {
continue
}
certEndDate, _ := time.Parse("2006-01-02", tea.StringValue(getUserCertificateDetailResp.Body.EndDate))
if time.Now().Before(certEndDate) {
continue
}
certificateIdsToDissociate = append(certificateIdsToDissociate, certificateId)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
// 关联监听和扩展证书
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-associateadditionalcertificateswithlistener
if !certificateIsAlreadyAssociated {
associateAdditionalCertificatesFromListenerReq := &alialb.AssociateAdditionalCertificatesWithListenerRequest{
ListenerId: tea.String(cloudListenerId),
Certificates: []*alialb.AssociateAdditionalCertificatesWithListenerRequestCertificates{
{
CertificateId: tea.String(cloudCertId),
},
},
}
associateAdditionalCertificatesFromListenerResp, err := d.sdkClients.ALB.AssociateAdditionalCertificatesWithListener(associateAdditionalCertificatesFromListenerReq)
d.logger.Debug("sdk request 'alb.AssociateAdditionalCertificatesWithListener'", slog.Any("request", associateAdditionalCertificatesFromListenerReq), slog.Any("response", associateAdditionalCertificatesFromListenerResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.AssociateAdditionalCertificatesWithListener': %w", err)
}
}
// 解除关联监听和扩展证书
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-dissociateadditionalcertificatesfromlistener
if !certificateIsAlreadyAssociated && len(certificateIdsToDissociate) > 0 {
dissociateAdditionalCertificates := make([]*alialb.DissociateAdditionalCertificatesFromListenerRequestCertificates, 0)
for _, certificateId := range certificateIdsToDissociate {
dissociateAdditionalCertificates = append(dissociateAdditionalCertificates, &alialb.DissociateAdditionalCertificatesFromListenerRequestCertificates{
CertificateId: tea.String(certificateId),
})
}
dissociateAdditionalCertificatesFromListenerReq := &alialb.DissociateAdditionalCertificatesFromListenerRequest{
ListenerId: tea.String(cloudListenerId),
Certificates: dissociateAdditionalCertificates,
}
dissociateAdditionalCertificatesFromListenerResp, err := d.sdkClients.ALB.DissociateAdditionalCertificatesFromListener(dissociateAdditionalCertificatesFromListenerReq)
d.logger.Debug("sdk request 'alb.DissociateAdditionalCertificatesFromListener'", slog.Any("request", dissociateAdditionalCertificatesFromListenerReq), slog.Any("response", dissociateAdditionalCertificatesFromListenerResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.DissociateAdditionalCertificatesFromListener': %w", err)
}
}
}
return nil
}
func createSDKClients(accessKeyId, accessKeySecret, region string) (*wSDKClients, error) {
// 接入点一览 https://api.aliyun.com/product/Alb
var albEndpoint string
switch region {
case "", "cn-hangzhou-finance":
albEndpoint = "alb.cn-hangzhou.aliyuncs.com"
default:
albEndpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region)
}
albConfig := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(albEndpoint),
}
albClient, err := alialb.NewClient(albConfig)
if err != nil {
return nil, err
}
// 接入点一览 https://api.aliyun.com/product/cas
var casEndpoint string
if !strings.HasPrefix(region, "cn-") {
casEndpoint = "cas.ap-southeast-1.aliyuncs.com"
} else {
casEndpoint = "cas.aliyuncs.com"
}
casConfig := &aliopen.Config{
Endpoint: tea.String(casEndpoint),
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
casClient, err := alicas.NewClient(casConfig)
if err != nil {
return nil, err
}
return &wSDKClients{
ALB: albClient,
CAS: casClient,
}, nil
}
func createSSLManager(accessKeyId, accessKeySecret, resourceGroupId, region string) (core.SSLManager, error) {
casRegion := region
if casRegion != "" {
// 阿里云 CAS 服务接入点是独立于 ALB 服务的
// 国内版固定接入点:华东一杭州
// 国际版固定接入点:亚太东南一新加坡
if !strings.HasPrefix(casRegion, "cn-") {
casRegion = "ap-southeast-1"
} else {
casRegion = "cn-hangzhou"
}
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
ResourceGroupId: resourceGroupId,
Region: casRegion,
})
return sslmgr, err
}

View File

@@ -0,0 +1,125 @@
package aliyunalb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-alb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fLoadbalancerId string
fListenerId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNALB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_alb_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNALB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNALB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNALB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNALB_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNALB_REGION="cn-hangzhou" \
--CERTIMATE_SSLDEPLOYER_ALIYUNALB_LOADBALANCERID="your-alb-instance-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNALB_LISTENERID="your-alb-listener-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNALB_DOMAIN="your-alb-sni-domain"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToLoadbalancer", 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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.RESOURCE_TYPE_LOADBALANCER,
LoadbalancerId: fLoadbalancerId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListener", 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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LISTENERID: %v", fListenerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.RESOURCE_TYPE_LISTENER,
ListenerId: fListenerId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,10 @@
package aliyunalb
type ResourceType string
const (
// 资源类型:部署到指定负载均衡器。
RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@@ -0,0 +1,277 @@
package aliyunapigw
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"time"
aliapig "github.com/alibabacloud-go/apig-20240327/v3/client"
alicloudapi "github.com/alibabacloud-go/cloudapi-20160714/v5/client"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aliyun-cas"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 服务类型。
ServiceType ServiceType `json:"serviceType"`
// API 网关 ID。
// 服务类型为 [SERVICE_TYPE_CLOUDNATIVE] 时必填。
GatewayId string `json:"gatewayId,omitempty"`
// API 分组 ID。
// 服务类型为 [SERVICE_TYPE_TRADITIONAL] 时必填。
GroupId string `json:"groupId,omitempty"`
// 自定义域名(支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClients *wSDKClients
sslManager core.SSLManager
}
type wSDKClients struct {
CloudNativeAPIGateway *aliapig.Client
TraditionalAPIGateway *alicloudapi.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
clients, err := createSDKClients(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := createSSLManager(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClients: clients,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
switch d.config.ServiceType {
case SERVICE_TYPE_TRADITIONAL:
if err := d.deployToTraditional(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
case SERVICE_TYPE_CLOUDNATIVE:
if err := d.deployToCloudNative(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported service type '%s'", string(d.config.ServiceType))
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToTraditional(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.GroupId == "" {
return errors.New("config `groupId` is required")
}
if d.config.Domain == "" {
return errors.New("config `domain` is required")
}
// 为自定义域名添加 SSL 证书
// REF: https://help.aliyun.com/zh/api-gateway/traditional-api-gateway/developer-reference/api-cloudapi-2016-07-14-setdomaincertificate
setDomainCertificateReq := &alicloudapi.SetDomainCertificateRequest{
GroupId: tea.String(d.config.GroupId),
DomainName: tea.String(d.config.Domain),
CertificateName: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
CertificateBody: tea.String(certPEM),
CertificatePrivateKey: tea.String(privkeyPEM),
}
setDomainCertificateResp, err := d.sdkClients.TraditionalAPIGateway.SetDomainCertificate(setDomainCertificateReq)
d.logger.Debug("sdk request 'apigateway.SetDomainCertificate'", slog.Any("request", setDomainCertificateReq), slog.Any("response", setDomainCertificateResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'apigateway.SetDomainCertificate': %w", err)
}
return nil
}
func (d *SSLDeployerProvider) deployToCloudNative(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.GatewayId == "" {
return errors.New("config `gatewayId` is required")
}
if d.config.Domain == "" {
return errors.New("config `domain` is required")
}
// 遍历查询域名列表,获取域名 ID
// REF: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/developer-reference/api-apig-2024-03-27-listdomains
var domainId string
listDomainsPageNumber := int32(1)
listDomainsPageSize := int32(10)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listDomainsReq := &aliapig.ListDomainsRequest{
ResourceGroupId: xtypes.ToPtrOrZeroNil(d.config.ResourceGroupId),
GatewayId: tea.String(d.config.GatewayId),
NameLike: tea.String(d.config.Domain),
PageNumber: tea.Int32(listDomainsPageNumber),
PageSize: tea.Int32(listDomainsPageSize),
}
listDomainsResp, err := d.sdkClients.CloudNativeAPIGateway.ListDomains(listDomainsReq)
d.logger.Debug("sdk request 'apig.ListDomains'", slog.Any("request", listDomainsReq), slog.Any("response", listDomainsResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'apig.ListDomains': %w", err)
}
if listDomainsResp.Body.Data.Items != nil {
for _, domainInfo := range listDomainsResp.Body.Data.Items {
if strings.EqualFold(tea.StringValue(domainInfo.Name), d.config.Domain) {
domainId = tea.StringValue(domainInfo.DomainId)
break
}
}
if domainId != "" {
break
}
}
if listDomainsResp.Body.Data.Items == nil || len(listDomainsResp.Body.Data.Items) < int(listDomainsPageSize) {
break
} else {
listDomainsPageNumber++
}
}
if domainId == "" {
return errors.New("domain not found")
}
// 查询域名
// REF: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/developer-reference/api-apig-2024-03-27-getdomain
getDomainReq := &aliapig.GetDomainRequest{}
getDomainResp, err := d.sdkClients.CloudNativeAPIGateway.GetDomain(tea.String(domainId), getDomainReq)
d.logger.Debug("sdk request 'apig.GetDomain'", slog.Any("domainId", domainId), slog.Any("request", getDomainReq), slog.Any("response", getDomainResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'apig.GetDomain': %w", err)
}
// 上传证书
upres, err := d.sslManager.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 更新域名
// REF: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/developer-reference/api-apig-2024-03-27-updatedomain
updateDomainReq := &aliapig.UpdateDomainRequest{
Protocol: tea.String("HTTPS"),
ForceHttps: getDomainResp.Body.Data.ForceHttps,
MTLSEnabled: getDomainResp.Body.Data.MTLSEnabled,
Http2Option: getDomainResp.Body.Data.Http2Option,
TlsMin: getDomainResp.Body.Data.TlsMin,
TlsMax: getDomainResp.Body.Data.TlsMax,
TlsCipherSuitesConfig: getDomainResp.Body.Data.TlsCipherSuitesConfig,
CertIdentifier: tea.String(upres.ExtendedData["certIdentifier"].(string)),
}
updateDomainResp, err := d.sdkClients.CloudNativeAPIGateway.UpdateDomain(tea.String(domainId), updateDomainReq)
d.logger.Debug("sdk request 'apig.UpdateDomain'", slog.Any("domainId", domainId), slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'apig.UpdateDomain': %w", err)
}
return nil
}
func createSDKClients(accessKeyId, accessKeySecret, region string) (*wSDKClients, error) {
// 接入点一览 https://api.aliyun.com/product/APIG
cloudNativeAPIGEndpoint := strings.ReplaceAll(fmt.Sprintf("apig.%s.aliyuncs.com", region), "..", ".")
cloudNativeAPIGConfig := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(cloudNativeAPIGEndpoint),
}
cloudNativeAPIGClient, err := aliapig.NewClient(cloudNativeAPIGConfig)
if err != nil {
return nil, err
}
// 接入点一览 https://api.aliyun.com/product/CloudAPI
traditionalAPIGEndpoint := strings.ReplaceAll(fmt.Sprintf("apigateway.%s.aliyuncs.com", region), "..", ".")
traditionalAPIGConfig := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(traditionalAPIGEndpoint),
}
traditionalAPIGClient, err := alicloudapi.NewClient(traditionalAPIGConfig)
if err != nil {
return nil, err
}
return &wSDKClients{
CloudNativeAPIGateway: cloudNativeAPIGClient,
TraditionalAPIGateway: traditionalAPIGClient,
}, nil
}
func createSSLManager(accessKeyId, accessKeySecret, resourceGroupId, region string) (core.SSLManager, error) {
casRegion := region
if casRegion != "" {
// 阿里云 CAS 服务接入点是独立于 APIGateway 服务的
// 国内版固定接入点:华东一杭州
// 国际版固定接入点:亚太东南一新加坡
if !strings.HasPrefix(casRegion, "cn-") {
casRegion = "ap-southeast-1"
} else {
casRegion = "cn-hangzhou"
}
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
ResourceGroupId: resourceGroupId,
Region: casRegion,
})
return sslmgr, err
}

View File

@@ -0,0 +1,95 @@
package aliyunapigw_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-apigw"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fServiceType string
fGatewayId string
fGroupId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNAPIGW_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fGatewayId, argsPrefix+"GATEWARYID", "", "")
flag.StringVar(&fGroupId, argsPrefix+"GROUPID", "", "")
flag.StringVar(&fServiceType, argsPrefix+"SERVICETYPE", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_apigw_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNAPIGW_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNAPIGW_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNAPIGW_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNAPIGW_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNAPIGW_REGION="cn-hangzhou" \
--CERTIMATE_SSLDEPLOYER_ALIYUNAPIGW_GATEWAYID="your-api-gateway-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNAPIGW_GROUPID="your-api-group-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNAPIGW_SERVICETYPE="cloudnative" \
--CERTIMATE_SSLDEPLOYER_ALIYUNAPIGW_DOMAIN="example.com"
*/
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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("GATEWAYID: %v", fGatewayId),
fmt.Sprintf("GROUPID: %v", fGroupId),
fmt.Sprintf("SERVICETYPE: %v", fServiceType),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ServiceType: provider.ServiceType(fServiceType),
GatewayId: fGatewayId,
GroupId: fGroupId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,10 @@
package aliyunapigw
type ServiceType string
const (
// 服务类型:原 API 网关。
SERVICE_TYPE_TRADITIONAL = ServiceType("traditional")
// 服务类型:云原生 API 网关。
SERVICE_TYPE_CLOUDNATIVE = ServiceType("cloudnative")
)

View File

@@ -0,0 +1,201 @@
package aliyuncasdeploy
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"time"
alicas "github.com/alibabacloud-go/cas-20200407/v3/client"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aliyun-cas"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 阿里云云产品资源 ID 数组。
ResourceIds []string `json:"resourceIds"`
// 阿里云云联系人 ID 数组。
// 零值时使用账号下第一个联系人。
ContactIds []string `json:"contactIds"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *alicas.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := createSSLManager(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if len(d.config.ResourceIds) == 0 {
return nil, errors.New("config `resourceIds` is required")
}
// 上传证书
upres, err := d.sslManager.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))
}
contactIds := d.config.ContactIds
if len(contactIds) == 0 {
// 获取联系人列表
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listcontact
listContactReq := &alicas.ListContactRequest{
ShowSize: tea.Int32(1),
CurrentPage: tea.Int32(1),
}
listContactResp, err := d.sdkClient.ListContact(listContactReq)
d.logger.Debug("sdk request 'cas.ListContact'", slog.Any("request", listContactReq), slog.Any("response", listContactResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cas.ListContact': %w", err)
}
if len(listContactResp.Body.ContactList) > 0 {
contactIds = []string{fmt.Sprintf("%d", listContactResp.Body.ContactList[0].ContactId)}
}
}
// 创建部署任务
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-createdeploymentjob
createDeploymentJobReq := &alicas.CreateDeploymentJobRequest{
Name: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
JobType: tea.String("user"),
CertIds: tea.String(upres.CertId),
ResourceIds: tea.String(strings.Join(d.config.ResourceIds, ",")),
ContactIds: tea.String(strings.Join(contactIds, ",")),
}
createDeploymentJobResp, err := d.sdkClient.CreateDeploymentJob(createDeploymentJobReq)
d.logger.Debug("sdk request 'cas.CreateDeploymentJob'", slog.Any("request", createDeploymentJobReq), slog.Any("response", createDeploymentJobResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cas.CreateDeploymentJob': %w", err)
}
// 循环获取部署任务详情,等待任务状态变更
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-describedeploymentjob
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
describeDeploymentJobReq := &alicas.DescribeDeploymentJobRequest{
JobId: createDeploymentJobResp.Body.JobId,
}
describeDeploymentJobResp, err := d.sdkClient.DescribeDeploymentJob(describeDeploymentJobReq)
d.logger.Debug("sdk request 'cas.DescribeDeploymentJob'", slog.Any("request", describeDeploymentJobReq), slog.Any("response", describeDeploymentJobResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cas.DescribeDeploymentJob': %w", err)
}
if describeDeploymentJobResp.Body.Status == nil || *describeDeploymentJobResp.Body.Status == "editing" {
return nil, errors.New("unexpected deployment job status")
}
if *describeDeploymentJobResp.Body.Status == "success" || *describeDeploymentJobResp.Body.Status == "error" {
break
}
d.logger.Info("waiting for deployment job completion ...")
time.Sleep(time.Second * 5)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, accessKeySecret, region string) (*alicas.Client, error) {
// 接入点一览 https://api.aliyun.com/product/cas
var endpoint string
switch region {
case "", "cn-hangzhou":
endpoint = "cas.aliyuncs.com"
default:
endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region)
}
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(endpoint),
}
client, err := alicas.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}
func createSSLManager(accessKeyId, accessKeySecret, resourceGroupId, region string) (core.SSLManager, error) {
casRegion := region
if casRegion != "" {
// 阿里云 CAS 服务接入点是独立于其他服务的
// 国内版固定接入点:华东一杭州
// 国际版固定接入点:亚太东南一新加坡
if !strings.HasPrefix(casRegion, "cn-") {
casRegion = "ap-southeast-1"
} else {
casRegion = "cn-hangzhou"
}
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
ResourceGroupId: resourceGroupId,
Region: casRegion,
})
return sslmgr, err
}

View File

@@ -0,0 +1,74 @@
package aliyuncas
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aliyun-cas"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
AccessKeySecret: config.AccessKeySecret,
ResourceGroupId: config.ResourceGroupId,
Region: config.Region,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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))
}
return &core.SSLDeployResult{}, nil
}

View File

@@ -0,0 +1,97 @@
package aliyuncdn
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"time"
alicdn "github.com/alibabacloud-go/cdn-20180510/v5/client"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *alicdn.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// "*.example.com" → ".example.com",适配阿里云 CDN 要求的泛域名格式
domain := strings.TrimPrefix(d.config.Domain, "*")
// 设置 CDN 域名域名证书
// REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate
setCdnDomainSSLCertificateReq := &alicdn.SetCdnDomainSSLCertificateRequest{
DomainName: tea.String(domain),
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(certPEM),
SSLPri: tea.String(privkeyPEM),
}
setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateReq)
d.logger.Debug("sdk request 'cdn.SetCdnDomainSSLCertificate'", slog.Any("request", setCdnDomainSSLCertificateReq), slog.Any("response", setCdnDomainSSLCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, accessKeySecret string) (*alicdn.Client, error) {
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("cdn.aliyuncs.com"),
}
client, err := alicdn.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,75 @@
package aliyuncdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_cdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNCDN_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNCDN_DOMAIN="example.com"
*/
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),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,319 @@
package aliyunclb
import (
"context"
"errors"
"fmt"
"log/slog"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
alislb "github.com/alibabacloud-go/slb-20140515/v4/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aliyun-slb"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听端口。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerPort int32 `json:"listenerPort,omitempty"`
// SNI 域名(支持泛域名)。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。
Domain string `json:"domain,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *alislb.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := createSSLManager(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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))
}
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case RESOURCE_TYPE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute
describeLoadBalancerAttributeReq := &alislb.DescribeLoadBalancerAttributeRequest{
RegionId: tea.String(d.config.Region),
LoadBalancerId: tea.String(d.config.LoadbalancerId),
}
describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq)
d.logger.Debug("sdk request 'slb.DescribeLoadBalancerAttribute'", slog.Any("request", describeLoadBalancerAttributeReq), slog.Any("response", describeLoadBalancerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerAttribute': %w", err)
}
// 查询 HTTPS 监听列表
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerlisteners
listenerPorts := make([]int32, 0)
describeLoadBalancerListenersLimit := int32(100)
var describeLoadBalancerListenersToken *string = nil
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
describeLoadBalancerListenersReq := &alislb.DescribeLoadBalancerListenersRequest{
RegionId: tea.String(d.config.Region),
MaxResults: tea.Int32(describeLoadBalancerListenersLimit),
NextToken: describeLoadBalancerListenersToken,
LoadBalancerId: []*string{tea.String(d.config.LoadbalancerId)},
ListenerProtocol: tea.String("https"),
}
describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListeners(describeLoadBalancerListenersReq)
d.logger.Debug("sdk request 'slb.DescribeLoadBalancerListeners'", slog.Any("request", describeLoadBalancerListenersReq), slog.Any("response", describeLoadBalancerListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerListeners': %w", err)
}
if describeLoadBalancerListenersResp.Body.Listeners != nil {
for _, listener := range describeLoadBalancerListenersResp.Body.Listeners {
listenerPorts = append(listenerPorts, *listener.ListenerPort)
}
}
if len(describeLoadBalancerListenersResp.Body.Listeners) == 0 || describeLoadBalancerListenersResp.Body.NextToken == nil {
break
} else {
describeLoadBalancerListenersToken = describeLoadBalancerListenersResp.Body.NextToken
}
}
// 遍历更新监听证书
if len(listenerPorts) == 0 {
d.logger.Info("no clb listeners to deploy")
} else {
d.logger.Info("found https listeners to deploy", slog.Any("listenerPorts", listenerPorts))
var errs []error
for _, listenerPort := range listenerPorts {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerPort, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *SSLDeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerPort == 0 {
return errors.New("config `listenerPort` is required")
}
// 更新监听
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerPort, cloudCertId); err != nil {
return err
}
return nil
}
func (d *SSLDeployerProvider) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerPort int32, cloudCertId string) error {
// 查询监听配置
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute
describeLoadBalancerHTTPSListenerAttributeReq := &alislb.DescribeLoadBalancerHTTPSListenerAttributeRequest{
LoadBalancerId: tea.String(cloudLoadbalancerId),
ListenerPort: tea.Int32(cloudListenerPort),
}
describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq)
d.logger.Debug("sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute'", slog.Any("request", describeLoadBalancerHTTPSListenerAttributeReq), slog.Any("response", describeLoadBalancerHTTPSListenerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute': %w", err)
}
if d.config.Domain == "" {
// 未指定 SNI只需部署到监听器
// 修改监听配置
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute
setLoadBalancerHTTPSListenerAttributeReq := &alislb.SetLoadBalancerHTTPSListenerAttributeRequest{
RegionId: tea.String(d.config.Region),
LoadBalancerId: tea.String(cloudLoadbalancerId),
ListenerPort: tea.Int32(cloudListenerPort),
ServerCertificateId: tea.String(cloudCertId),
}
setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttribute(setLoadBalancerHTTPSListenerAttributeReq)
d.logger.Debug("sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute'", slog.Any("request", setLoadBalancerHTTPSListenerAttributeReq), slog.Any("response", setLoadBalancerHTTPSListenerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute': %w", err)
}
} else {
// 指定 SNI需部署到扩展域名
// 查询扩展域名
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions
describeDomainExtensionsReq := &alislb.DescribeDomainExtensionsRequest{
RegionId: tea.String(d.config.Region),
LoadBalancerId: tea.String(cloudLoadbalancerId),
ListenerPort: tea.Int32(cloudListenerPort),
}
describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensions(describeDomainExtensionsReq)
d.logger.Debug("sdk request 'slb.DescribeDomainExtensions'", slog.Any("request", describeDomainExtensionsReq), slog.Any("response", describeDomainExtensionsResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'slb.DescribeDomainExtensions': %w", err)
}
// 遍历修改扩展域名
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute
if describeDomainExtensionsResp.Body.DomainExtensions != nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension != nil {
var errs []error
for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension {
if *domainExtension.Domain != d.config.Domain {
continue
}
setDomainExtensionAttributeReq := &alislb.SetDomainExtensionAttributeRequest{
RegionId: tea.String(d.config.Region),
DomainExtensionId: tea.String(*domainExtension.DomainExtensionId),
ServerCertificateId: tea.String(cloudCertId),
}
setDomainExtensionAttributeResp, err := d.sdkClient.SetDomainExtensionAttribute(setDomainExtensionAttributeReq)
d.logger.Debug("sdk request 'slb.SetDomainExtensionAttribute'", slog.Any("request", setDomainExtensionAttributeReq), slog.Any("response", setDomainExtensionAttributeResp))
if err != nil {
errs = append(errs, fmt.Errorf("failed to execute sdk request 'slb.SetDomainExtensionAttribute': %w", err))
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
}
return nil
}
func createSDKClient(accessKeyId, accessKeySecret, region string) (*alislb.Client, error) {
// 接入点一览 https://api.aliyun.com/product/Slb
var endpoint string
switch region {
case "",
"cn-hangzhou",
"cn-hangzhou-finance",
"cn-shanghai-finance-1",
"cn-shenzhen-finance-1":
endpoint = "slb.aliyuncs.com"
default:
endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)
}
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(endpoint),
}
client, err := alislb.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}
func createSSLManager(accessKeyId, accessKeySecret, resourceGroupId, region string) (core.SSLManager, error) {
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
ResourceGroupId: resourceGroupId,
Region: region,
})
return sslmgr, err
}

View File

@@ -0,0 +1,126 @@
package aliyunclb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-clb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fLoadbalancerId string
fListenerPort int64
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNCLB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.Int64Var(&fListenerPort, argsPrefix+"LISTENERPORT", 443, "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_clb_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNCLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNCLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNCLB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNCLB_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNCLB_REGION="cn-hangzhou" \
--CERTIMATE_SSLDEPLOYER_ALIYUNCLB_LOADBALANCERID="your-clb-instance-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNCLB_LISTENERPORT=443 \
--CERTIMATE_SSLDEPLOYER_ALIYUNCLB_DOMAIN="your-clb-sni-domain"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToLoadbalancer", 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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.RESOURCE_TYPE_LOADBALANCER,
LoadbalancerId: fLoadbalancerId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListener", 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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("LISTENERPORT: %v", fListenerPort),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.RESOURCE_TYPE_LISTENER,
LoadbalancerId: fLoadbalancerId,
ListenerPort: int32(fListenerPort),
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,10 @@
package aliyunclb
type ResourceType string
const (
// 资源类型:部署到指定负载均衡器。
RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@@ -0,0 +1,101 @@
package aliyundcdn
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"time"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
alidcdn "github.com/alibabacloud-go/dcdn-20180115/v3/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *alidcdn.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// "*.example.com" → ".example.com",适配阿里云 DCDN 要求的泛域名格式
domain := strings.TrimPrefix(d.config.Domain, "*")
// 配置域名证书
// REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-setdcdndomainsslcertificate
setDcdnDomainSSLCertificateReq := &alidcdn.SetDcdnDomainSSLCertificateRequest{
DomainName: tea.String(domain),
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(certPEM),
SSLPri: tea.String(privkeyPEM),
}
setDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificate(setDcdnDomainSSLCertificateReq)
d.logger.Debug("sdk request 'dcdn.SetDcdnDomainSSLCertificate'", slog.Any("request", setDcdnDomainSSLCertificateReq), slog.Any("response", setDcdnDomainSSLCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'dcdn.SetDcdnDomainSSLCertificate': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, accessKeySecret string) (*alidcdn.Client, error) {
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("dcdn.aliyuncs.com"),
}
client, err := alidcdn.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,75 @@
package aliyundcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-dcdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_dcdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNDCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNDCDN_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNDCDN_DOMAIN="example.com"
*/
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),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,139 @@
package aliyunddos
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliddos "github.com/alibabacloud-go/ddoscoo-20200101/v4/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aliyun-slb"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 网站域名(支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *aliddos.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := createSSLManager(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 上传证书
upres, err := d.sslManager.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))
}
// 为网站业务转发规则关联 SSL 证书
// REF: https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-associatewebcert
certId, _ := strconv.Atoi(upres.CertId)
associateWebCertReq := &aliddos.AssociateWebCertRequest{
Domain: tea.String(d.config.Domain),
CertId: tea.Int32(int32(certId)),
}
associateWebCertResp, err := d.sdkClient.AssociateWebCert(associateWebCertReq)
d.logger.Debug("sdk request 'dcdn.AssociateWebCert'", slog.Any("request", associateWebCertReq), slog.Any("response", associateWebCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'dcdn.AssociateWebCert': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, accessKeySecret, region string) (*aliddos.Client, error) {
// 接入点一览 https://api.aliyun.com/product/ddoscoo
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(strings.ReplaceAll(fmt.Sprintf("ddoscoo.%s.aliyuncs.com", region), "..", ".")),
}
client, err := aliddos.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}
func createSSLManager(accessKeyId, accessKeySecret, resourceGroupId, region string) (core.SSLManager, error) {
casRegion := region
if casRegion != "" {
// 阿里云 CAS 服务接入点是独立于 Anti-DDoS 服务的
// 国内版固定接入点:华东一杭州
// 国际版固定接入点:亚太东南一新加坡
if !strings.HasPrefix(casRegion, "cn-") {
casRegion = "ap-southeast-1"
} else {
casRegion = "cn-hangzhou"
}
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
ResourceGroupId: resourceGroupId,
Region: casRegion,
})
return sslmgr, err
}

View File

@@ -0,0 +1,80 @@
package aliyunddos_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-ddos"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNDDOS_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_ddos_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNDDOS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNDDOS_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNDDOS_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNDDOS_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNDDOS_REGION="cn-hangzhou" \
--CERTIMATE_SSLDEPLOYER_ALIYUNDDOS_DOMAIN="example.com"
*/
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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,140 @@
package aliyunesa
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliesa "github.com/alibabacloud-go/esa-20240910/v2/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aliyun-cas"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 阿里云 ESA 站点 ID。
SiteId int64 `json:"siteId"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *aliesa.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := createSSLManager(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.SiteId == 0 {
return nil, errors.New("config `siteId` is required")
}
// 上传证书
upres, err := d.sslManager.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))
}
// 配置站点证书
// REF: https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-setcertificate
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
setCertificateReq := &aliesa.SetCertificateRequest{
SiteId: tea.Int64(d.config.SiteId),
Type: tea.String("cas"),
CasId: tea.Int64(certId),
}
setCertificateResp, err := d.sdkClient.SetCertificate(setCertificateReq)
d.logger.Debug("sdk request 'esa.SetCertificate'", slog.Any("request", setCertificateReq), slog.Any("response", setCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'esa.SetCertificate': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, accessKeySecret, region string) (*aliesa.Client, error) {
// 接入点一览 https://api.aliyun.com/product/ESA
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(strings.ReplaceAll(fmt.Sprintf("esa.%s.aliyuncs.com", region), "..", ".")),
}
client, err := aliesa.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}
func createSSLManager(accessKeyId, accessKeySecret, resourceGroupId, region string) (core.SSLManager, error) {
casRegion := region
if casRegion != "" {
// 阿里云 CAS 服务接入点是独立于 ESA 服务的
// 国内版固定接入点:华东一杭州
// 国际版固定接入点:亚太东南一新加坡
if !strings.HasPrefix(casRegion, "cn-") {
casRegion = "ap-southeast-1"
} else {
casRegion = "cn-hangzhou"
}
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
ResourceGroupId: resourceGroupId,
Region: casRegion,
})
return sslmgr, err
}

View File

@@ -0,0 +1,80 @@
package aliyunesa_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-esa"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fSiteId int64
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNESA_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.Int64Var(&fSiteId, argsPrefix+"SITEID", 0, "")
}
/*
Shell command to run this test:
go test -v ./aliyun_esa_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNESA_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNESA_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNESA_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNESA_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNESA_REGION="cn-hangzhou" \
--CERTIMATE_SSLDEPLOYER_ALIYUNESA_SITEID="your-esa-site-id"
*/
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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("SITEID: %v", fSiteId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
SiteId: fSiteId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,203 @@
package aliyunfc
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"time"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
alifc3 "github.com/alibabacloud-go/fc-20230330/v4/client"
alifc2 "github.com/alibabacloud-go/fc-open-20210406/v2/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 服务版本。
// 可取值 "2.0"、"3.0"。
ServiceVersion string `json:"serviceVersion"`
// 自定义域名(支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClients *wSDKClients
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
type wSDKClients struct {
FC2 *alifc2.Client
FC3 *alifc3.Client
}
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
clients, err := createSDKClients(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClients: clients,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
switch d.config.ServiceVersion {
case "3", "3.0":
if err := d.deployToFC3(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
case "2", "2.0":
if err := d.deployToFC2(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported service version '%s'", d.config.ServiceVersion)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToFC3(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.Domain == "" {
return errors.New("config `domain` is required")
}
// 获取自定义域名
// REF: https://help.aliyun.com/zh/functioncompute/fc-3-0/developer-reference/api-fc-2023-03-30-getcustomdomain
getCustomDomainResp, err := d.sdkClients.FC3.GetCustomDomain(tea.String(d.config.Domain))
d.logger.Debug("sdk request 'fc.GetCustomDomain'", slog.Any("response", getCustomDomainResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'fc.GetCustomDomain': %w", err)
}
// 更新自定义域名
// REF: https://help.aliyun.com/zh/functioncompute/fc-3-0/developer-reference/api-fc-2023-03-30-updatecustomdomain
updateCustomDomainReq := &alifc3.UpdateCustomDomainRequest{
Body: &alifc3.UpdateCustomDomainInput{
CertConfig: &alifc3.CertConfig{
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
Certificate: tea.String(certPEM),
PrivateKey: tea.String(privkeyPEM),
},
Protocol: getCustomDomainResp.Body.Protocol,
TlsConfig: getCustomDomainResp.Body.TlsConfig,
},
}
if tea.StringValue(updateCustomDomainReq.Body.Protocol) == "HTTP" {
updateCustomDomainReq.Body.Protocol = tea.String("HTTP,HTTPS")
}
updateCustomDomainResp, err := d.sdkClients.FC3.UpdateCustomDomain(tea.String(d.config.Domain), updateCustomDomainReq)
d.logger.Debug("sdk request 'fc.UpdateCustomDomain'", slog.Any("request", updateCustomDomainReq), slog.Any("response", updateCustomDomainResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'fc.UpdateCustomDomain': %w", err)
}
return nil
}
func (d *SSLDeployerProvider) deployToFC2(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.Domain == "" {
return errors.New("config `domain` is required")
}
// 获取自定义域名
// REF: https://help.aliyun.com/zh/functioncompute/fc-2-0/developer-reference/api-fc-open-2021-04-06-getcustomdomain
getCustomDomainResp, err := d.sdkClients.FC2.GetCustomDomain(tea.String(d.config.Domain))
d.logger.Debug("sdk request 'fc.GetCustomDomain'", slog.Any("response", getCustomDomainResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'fc.GetCustomDomain': %w", err)
}
// 更新自定义域名
// REF: https://help.aliyun.com/zh/functioncompute/fc-2-0/developer-reference/api-fc-open-2021-04-06-updatecustomdomain
updateCustomDomainReq := &alifc2.UpdateCustomDomainRequest{
CertConfig: &alifc2.CertConfig{
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
Certificate: tea.String(certPEM),
PrivateKey: tea.String(privkeyPEM),
},
Protocol: getCustomDomainResp.Body.Protocol,
TlsConfig: getCustomDomainResp.Body.TlsConfig,
}
if tea.StringValue(updateCustomDomainReq.Protocol) == "HTTP" {
updateCustomDomainReq.Protocol = tea.String("HTTP,HTTPS")
}
updateCustomDomainResp, err := d.sdkClients.FC2.UpdateCustomDomain(tea.String(d.config.Domain), updateCustomDomainReq)
d.logger.Debug("sdk request 'fc.UpdateCustomDomain'", slog.Any("request", updateCustomDomainReq), slog.Any("response", updateCustomDomainResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'fc.UpdateCustomDomain': %w", err)
}
return nil
}
func createSDKClients(accessKeyId, accessKeySecret, region string) (*wSDKClients, error) {
// 接入点一览 https://api.aliyun.com/product/FC-Open
var fc2Endpoint string
switch region {
case "":
fc2Endpoint = "fc.aliyuncs.com"
case "cn-hangzhou-finance":
fc2Endpoint = fmt.Sprintf("%s.fc.aliyuncs.com", region)
default:
fc2Endpoint = fmt.Sprintf("fc.%s.aliyuncs.com", region)
}
fc2Config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(fc2Endpoint),
}
fc2Client, err := alifc2.NewClient(fc2Config)
if err != nil {
return nil, err
}
// 接入点一览 https://api.aliyun.com/product/FC-Open
fc3Endpoint := strings.ReplaceAll(fmt.Sprintf("fcv3.%s.aliyuncs.com", region), "..", ".")
fc3Config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(fc3Endpoint),
}
fc3Client, err := alifc3.NewClient(fc3Config)
if err != nil {
return nil, err
}
return &wSDKClients{
FC2: fc2Client,
FC3: fc3Client,
}, nil
}

View File

@@ -0,0 +1,80 @@
package aliyunfc_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-fc"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNFC_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_fc_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNFC_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNFC_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNFC_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNFC_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNFC_REGION="cn-hangzhou" \
--CERTIMATE_SSLDEPLOYER_ALIYUNFC_DOMAIN="example.com"
*/
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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,325 @@
package aliyunga
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliga "github.com/alibabacloud-go/ga-20191120/v3/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aliyun-cas"
xslices "github.com/usual2970/certimate/internal/pkg/utils/slices"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 全球加速实例 ID。
AcceleratorId string `json:"acceleratorId"`
// 全球加速监听 ID。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
// SNI 域名(不支持泛域名)。
// 部署资源类型为 [RESOURCE_TYPE_ACCELERATOR]、[RESOURCE_TYPE_LISTENER] 时选填。
Domain string `json:"domain,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *aliga.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := createSSLManager(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId)
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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))
}
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_ACCELERATOR:
if err := d.deployToAccelerator(ctx, upres.ExtendedData["certIdentifier"].(string)); err != nil {
return nil, err
}
case RESOURCE_TYPE_LISTENER:
if err := d.deployToListener(ctx, upres.ExtendedData["certIdentifier"].(string)); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToAccelerator(ctx context.Context, cloudCertId string) error {
if d.config.AcceleratorId == "" {
return errors.New("config `acceleratorId` is required")
}
// 查询 HTTPS 监听列表
// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-listlisteners
listenerIds := make([]string, 0)
listListenersPageNumber := int32(1)
listListenersPageSize := int32(50)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenersReq := &aliga.ListListenersRequest{
RegionId: tea.String("cn-hangzhou"),
AcceleratorId: tea.String(d.config.AcceleratorId),
PageNumber: tea.Int32(listListenersPageNumber),
PageSize: tea.Int32(listListenersPageSize),
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
d.logger.Debug("sdk request 'ga.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'ga.ListListeners': %w", err)
}
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
if strings.EqualFold(tea.StringValue(listener.Protocol), "https") {
listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))
}
}
}
if len(listListenersResp.Body.Listeners) < int(listListenersPageSize) {
break
} else {
listListenersPageNumber++
}
}
// 遍历更新监听证书
if len(listenerIds) == 0 {
d.logger.Info("no ga listeners to deploy")
} else {
var errs []error
d.logger.Info("found https listeners to deploy", slog.Any("listenerIds", listenerIds))
for _, listenerId := range listenerIds {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.AcceleratorId, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *SSLDeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.AcceleratorId == "" {
return errors.New("config `acceleratorId` is required")
}
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 更新监听
if err := d.updateListenerCertificate(ctx, d.config.AcceleratorId, d.config.ListenerId, cloudCertId); err != nil {
return err
}
return nil
}
func (d *SSLDeployerProvider) updateListenerCertificate(ctx context.Context, cloudAcceleratorId string, cloudListenerId string, cloudCertId string) error {
// 查询监听绑定的证书列表
// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-listlistenercertificates
var listenerDefaultCertificate *aliga.ListListenerCertificatesResponseBodyCertificates
var listenerAdditionalCertificates []*aliga.ListListenerCertificatesResponseBodyCertificates = make([]*aliga.ListListenerCertificatesResponseBodyCertificates, 0)
var listListenerCertificatesNextToken *string
for {
listListenerCertificatesReq := &aliga.ListListenerCertificatesRequest{
RegionId: tea.String("cn-hangzhou"),
AcceleratorId: tea.String(cloudAcceleratorId),
ListenerId: tea.String(cloudListenerId),
NextToken: listListenerCertificatesNextToken,
MaxResults: tea.Int32(20),
}
listListenerCertificatesResp, err := d.sdkClient.ListListenerCertificates(listListenerCertificatesReq)
d.logger.Debug("sdk request 'ga.ListListenerCertificates'", slog.Any("request", listListenerCertificatesReq), slog.Any("response", listListenerCertificatesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'ga.ListListenerCertificates': %w", err)
}
if listListenerCertificatesResp.Body.Certificates != nil {
for _, certificate := range listListenerCertificatesResp.Body.Certificates {
if tea.BoolValue(certificate.IsDefault) {
listenerDefaultCertificate = certificate
} else {
listenerAdditionalCertificates = append(listenerAdditionalCertificates, certificate)
}
}
}
if listListenerCertificatesResp.Body.NextToken == nil {
break
} else {
listListenerCertificatesNextToken = listListenerCertificatesResp.Body.NextToken
}
}
if d.config.Domain == "" {
// 未指定 SNI只需部署到监听器
if listenerDefaultCertificate != nil && tea.StringValue(listenerDefaultCertificate.CertificateId) == cloudCertId {
d.logger.Info("no need to update ga listener default certificate")
return nil
}
// 修改监听的属性
// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-updatelistener
updateListenerReq := &aliga.UpdateListenerRequest{
RegionId: tea.String("cn-hangzhou"),
ListenerId: tea.String(cloudListenerId),
Certificates: []*aliga.UpdateListenerRequestCertificates{{
Id: tea.String(cloudCertId),
}},
}
updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)
d.logger.Debug("sdk request 'ga.UpdateListener'", slog.Any("request", updateListenerReq), slog.Any("response", updateListenerResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'ga.UpdateListener': %w", err)
}
} else {
// 指定 SNI需部署到扩展域名
if xslices.Some(listenerAdditionalCertificates, func(item *aliga.ListListenerCertificatesResponseBodyCertificates) bool {
return tea.StringValue(item.CertificateId) == cloudCertId
}) {
d.logger.Info("no need to update ga listener additional certificate")
return nil
}
if xslices.Some(listenerAdditionalCertificates, func(item *aliga.ListListenerCertificatesResponseBodyCertificates) bool {
return tea.StringValue(item.Domain) == d.config.Domain
}) {
// 为监听替换扩展证书
// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-updateadditionalcertificatewithlistener
updateAdditionalCertificateWithListenerReq := &aliga.UpdateAdditionalCertificateWithListenerRequest{
RegionId: tea.String("cn-hangzhou"),
AcceleratorId: tea.String(cloudAcceleratorId),
ListenerId: tea.String(cloudListenerId),
CertificateId: tea.String(cloudCertId),
Domain: tea.String(d.config.Domain),
}
updateAdditionalCertificateWithListenerResp, err := d.sdkClient.UpdateAdditionalCertificateWithListener(updateAdditionalCertificateWithListenerReq)
d.logger.Debug("sdk request 'ga.UpdateAdditionalCertificateWithListener'", slog.Any("request", updateAdditionalCertificateWithListenerReq), slog.Any("response", updateAdditionalCertificateWithListenerResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'ga.UpdateAdditionalCertificateWithListener': %w", err)
}
} else {
// 为监听绑定扩展证书
// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-associateadditionalcertificateswithlistener
associateAdditionalCertificatesWithListenerReq := &aliga.AssociateAdditionalCertificatesWithListenerRequest{
RegionId: tea.String("cn-hangzhou"),
AcceleratorId: tea.String(cloudAcceleratorId),
ListenerId: tea.String(cloudListenerId),
Certificates: []*aliga.AssociateAdditionalCertificatesWithListenerRequestCertificates{{
Id: tea.String(cloudCertId),
Domain: tea.String(d.config.Domain),
}},
}
associateAdditionalCertificatesWithListenerResp, err := d.sdkClient.AssociateAdditionalCertificatesWithListener(associateAdditionalCertificatesWithListenerReq)
d.logger.Debug("sdk request 'ga.AssociateAdditionalCertificatesWithListener'", slog.Any("request", associateAdditionalCertificatesWithListenerReq), slog.Any("response", associateAdditionalCertificatesWithListenerResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'ga.AssociateAdditionalCertificatesWithListener': %w", err)
}
}
}
return nil
}
func createSDKClient(accessKeyId, accessKeySecret string) (*aliga.Client, error) {
// 接入点一览 https://api.aliyun.com/product/Ga
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("ga.cn-hangzhou.aliyuncs.com"),
}
client, err := aliga.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}
func createSSLManager(accessKeyId, accessKeySecret, resourceGroupId string) (core.SSLManager, error) {
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
ResourceGroupId: resourceGroupId,
Region: "cn-hangzhou",
})
return sslmgr, err
}

View File

@@ -0,0 +1,118 @@
package aliyunga_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-ga"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fAcceleratorId string
fListenerId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNGA_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fAcceleratorId, argsPrefix+"ACCELERATORID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_ga_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNGA_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNGA_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNGA_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNGA_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNGA_ACCELERATORID="your-ga-accelerator-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNGA_LISTENERID="your-ga-listener-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNGA_DOMAIN="your-ga-sni-domain"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToAccelerator", 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),
fmt.Sprintf("ACCELERATORID: %v", fAcceleratorId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
ResourceType: provider.RESOURCE_TYPE_ACCELERATOR,
AcceleratorId: fAcceleratorId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListener", 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),
fmt.Sprintf("LISTENERID: %v", fListenerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
ResourceType: provider.RESOURCE_TYPE_LISTENER,
ListenerId: fListenerId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,10 @@
package aliyunga
type ResourceType string
const (
// 资源类型:部署到指定全球加速器。
RESOURCE_TYPE_ACCELERATOR = ResourceType("accelerator")
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@@ -0,0 +1,119 @@
package aliyunlive
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"time"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
alilive "github.com/alibabacloud-go/live-20161101/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 直播流域名(支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *alilive.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// "*.example.com" → ".example.com",适配阿里云 Live 要求的泛域名格式
domain := strings.TrimPrefix(d.config.Domain, "*")
// 设置域名证书
// REF: https://help.aliyun.com/zh/live/developer-reference/api-live-2016-11-01-setlivedomaincertificate
setLiveDomainSSLCertificateReq := &alilive.SetLiveDomainCertificateRequest{
DomainName: tea.String(domain),
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(certPEM),
SSLPri: tea.String(privkeyPEM),
}
setLiveDomainSSLCertificateResp, err := d.sdkClient.SetLiveDomainCertificate(setLiveDomainSSLCertificateReq)
d.logger.Debug("sdk request 'live.SetLiveDomainCertificate'", slog.Any("request", setLiveDomainSSLCertificateReq), slog.Any("response", setLiveDomainSSLCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'live.SetLiveDomainCertificate': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, accessKeySecret, region string) (*alilive.Client, error) {
// 接入点一览 https://api.aliyun.com/product/live
var endpoint string
switch region {
case "",
"cn-qingdao",
"cn-beijing",
"cn-shanghai",
"cn-shenzhen",
"ap-northeast-1",
"ap-southeast-5",
"me-central-1":
endpoint = "live.aliyuncs.com"
default:
endpoint = fmt.Sprintf("live.%s.aliyuncs.com", region)
}
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(endpoint),
}
client, err := alilive.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,80 @@
package aliyunlive_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-live"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNLIVE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_live_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNLIVE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNLIVE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNLIVE_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNLIVE_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNLIVE_REGION="cn-hangzhou" \
--CERTIMATE_SSLDEPLOYER_ALIYUNLIVE_DOMAIN="example.com"
*/
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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,263 @@
package aliyunnlb
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
alinlb "github.com/alibabacloud-go/nlb-20220430/v2/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aliyun-cas"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听 ID。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *alinlb.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := createSSLManager(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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))
}
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case RESOURCE_TYPE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute
getLoadBalancerAttributeReq := &alinlb.GetLoadBalancerAttributeRequest{
LoadBalancerId: tea.String(d.config.LoadbalancerId),
}
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
d.logger.Debug("sdk request 'nlb.GetLoadBalancerAttribute'", slog.Any("request", getLoadBalancerAttributeReq), slog.Any("response", getLoadBalancerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nlb.GetLoadBalancerAttribute': %w", err)
}
// 查询 TCPSSL 监听列表
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-listlisteners
listenerIds := make([]string, 0)
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenersReq := &alinlb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
ListenerProtocol: tea.String("TCPSSL"),
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
d.logger.Debug("sdk request 'nlb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nlb.ListListeners': %w", err)
}
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))
}
}
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = listListenersResp.Body.NextToken
}
}
// 遍历更新监听证书
if len(listenerIds) == 0 {
d.logger.Info("no nlb listeners to deploy")
} else {
d.logger.Info("found tcpssl listeners to deploy", slog.Any("listenerIds", listenerIds))
var errs []error
for _, listenerId := range listenerIds {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *SSLDeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 更新监听
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
return err
}
return nil
}
func (d *SSLDeployerProvider) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
// 查询监听的属性
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute
getListenerAttributeReq := &alinlb.GetListenerAttributeRequest{
ListenerId: tea.String(cloudListenerId),
}
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
d.logger.Debug("sdk request 'nlb.GetListenerAttribute'", slog.Any("request", getListenerAttributeReq), slog.Any("response", getListenerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nlb.GetListenerAttribute': %w", err)
}
// 修改监听的属性
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute
updateListenerAttributeReq := &alinlb.UpdateListenerAttributeRequest{
ListenerId: tea.String(cloudListenerId),
CertificateIds: []*string{tea.String(cloudCertId)},
}
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
d.logger.Debug("sdk request 'nlb.UpdateListenerAttribute'", slog.Any("request", updateListenerAttributeReq), slog.Any("response", updateListenerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nlb.UpdateListenerAttribute': %w", err)
}
return nil
}
func createSDKClient(accessKeyId, accessKeySecret, region string) (*alinlb.Client, error) {
// 接入点一览 https://api.aliyun.com/product/Nlb
endpoint := strings.ReplaceAll(fmt.Sprintf("nlb.%s.aliyuncs.com", region), "..", ".")
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(endpoint),
}
client, err := alinlb.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}
func createSSLManager(accessKeyId, accessKeySecret, resourceGroupId, region string) (core.SSLManager, error) {
casRegion := region
if casRegion != "" {
// 阿里云 CAS 服务接入点是独立于 NLB 服务的
// 国内版固定接入点:华东一杭州
// 国际版固定接入点:亚太东南一新加坡
if !strings.HasPrefix(casRegion, "cn-") {
casRegion = "ap-southeast-1"
} else {
casRegion = "cn-hangzhou"
}
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
ResourceGroupId: resourceGroupId,
Region: casRegion,
})
return sslmgr, err
}

View File

@@ -0,0 +1,119 @@
package aliyunnlb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-nlb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fLoadbalancerId string
fListenerId string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNNLB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_nlb_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNNLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNNLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNNLB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNNLB_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNNLB_REGION="cn-hangzhou" \
--CERTIMATE_SSLDEPLOYER_ALIYUNNLB_LOADBALANCERID="your-nlb-instance-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNNLB_LISTENERID="your-nlb-listener-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToLoadbalancer", 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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.RESOURCE_TYPE_LOADBALANCER,
LoadbalancerId: fLoadbalancerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListener", 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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("LISTENERID: %v", fListenerId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: provider.RESOURCE_TYPE_LISTENER,
ListenerId: fListenerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,10 @@
package aliyunnlb
type ResourceType string
const (
// 资源类型:部署到指定负载均衡器。
RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@@ -0,0 +1,115 @@
package aliyunoss
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/usual2970/certimate/internal/pkg/core"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 存储桶名。
Bucket string `json:"bucket"`
// 自定义域名(不支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *oss.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Bucket == "" {
return nil, errors.New("config `bucket` is required")
}
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 为存储空间绑定自定义域名
// REF: https://help.aliyun.com/zh/oss/developer-reference/putcname
putBucketCnameWithCertificateReq := oss.PutBucketCname{
Cname: d.config.Domain,
CertificateConfiguration: &oss.CertificateConfiguration{
Certificate: certPEM,
PrivateKey: privkeyPEM,
Force: true,
},
}
err := d.sdkClient.PutBucketCnameWithCertificate(d.config.Bucket, putBucketCnameWithCertificateReq)
d.logger.Debug("sdk request 'oss.PutBucketCnameWithCertificate'", slog.Any("bucket", d.config.Bucket), slog.Any("request", putBucketCnameWithCertificateReq))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'oss.PutBucketCnameWithCertificate': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, accessKeySecret, region string) (*oss.Client, error) {
// 接入点一览 https://api.aliyun.com/product/Oss
var endpoint string
switch region {
case "":
endpoint = "oss.aliyuncs.com"
case
"cn-hzjbp",
"cn-hzjbp-a",
"cn-hzjbp-b":
endpoint = "oss-cn-hzjbp-a-internal.aliyuncs.com"
case
"cn-shanghai-finance-1",
"cn-shenzhen-finance-1",
"cn-beijing-finance-1",
"cn-north-2-gov-1":
endpoint = fmt.Sprintf("oss-%s-internal.aliyuncs.com", region)
default:
endpoint = fmt.Sprintf("oss-%s.aliyuncs.com", region)
}
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,85 @@
package aliyunoss_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-oss"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fBucket string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNOSS_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_oss_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNOSS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNOSS_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNOSS_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNOSS_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNOSS_REGION="cn-hangzhou" \
--CERTIMATE_SSLDEPLOYER_ALIYUNOSS_BUCKET="your-oss-bucket" \
--CERTIMATE_SSLDEPLOYER_ALIYUNOSS_DOMAIN="example.com"
*/
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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("BUCKET: %v", fBucket),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
Bucket: fBucket,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,102 @@
package aliyunvod
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"time"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
alivod "github.com/alibabacloud-go/vod-20170321/v4/client"
"github.com/usual2970/certimate/internal/pkg/core"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 点播加速域名(不支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *alivod.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 设置域名证书
// REF: https://help.aliyun.com/zh/vod/developer-reference/api-vod-2017-03-21-setvoddomainsslcertificate
setVodDomainSSLCertificateReq := &alivod.SetVodDomainSSLCertificateRequest{
DomainName: tea.String(d.config.Domain),
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(certPEM),
SSLPri: tea.String(privkeyPEM),
}
setVodDomainSSLCertificateResp, err := d.sdkClient.SetVodDomainSSLCertificate(setVodDomainSSLCertificateReq)
d.logger.Debug("sdk request 'live.SetVodDomainSSLCertificate'", slog.Any("request", setVodDomainSSLCertificateReq), slog.Any("response", setVodDomainSSLCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'live.SetVodDomainSSLCertificate': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, accessKeySecret, region string) (*alivod.Client, error) {
// 接入点一览 https://api.aliyun.com/product/vod
endpoint := strings.ReplaceAll(fmt.Sprintf("vod.%s.aliyuncs.com", region), "..", ".")
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(endpoint),
}
client, err := alivod.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,80 @@
package aliyunvod_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-vod"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNVOD_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_vod_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNVOD_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNVOD_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNVOD_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNVOD_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNVOD_REGION="cn-hangzhou" \
--CERTIMATE_SSLDEPLOYER_ALIYUNVOD_DOMAIN="example.com"
*/
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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,377 @@
package aliyunwaf
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
aliwaf "github.com/alibabacloud-go/waf-openapi-20211001/v5/client"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aliyun-cas"
xslices "github.com/usual2970/certimate/internal/pkg/utils/slices"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 阿里云资源组 ID。
ResourceGroupId string `json:"resourceGroupId,omitempty"`
// 阿里云地域。
Region string `json:"region"`
// 服务版本。
ServiceVersion string `json:"serviceVersion"`
// WAF 实例 ID。
InstanceId string `json:"instanceId"`
// 接入域名(支持泛域名)。
Domain string `json:"domain,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *aliwaf.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := createSSLManager(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.InstanceId == "" {
return nil, errors.New("config `instanceId` is required")
}
switch d.config.ServiceVersion {
case "3", "3.0":
if err := d.deployToWAF3(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported service version '%s'", d.config.ServiceVersion)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToWAF3(ctx context.Context, certPEM string, privkeyPEM string) error {
// 上传证书
upres, err := d.sslManager.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
if d.config.Domain == "" {
// 未指定接入域名,只需替换默认证书即可
// 查询默认 SSL/TLS 设置
// REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describedefaulthttps
describeDefaultHttpsReq := &aliwaf.DescribeDefaultHttpsRequest{
ResourceManagerResourceGroupId: xtypes.ToPtrOrZeroNil(d.config.ResourceGroupId),
InstanceId: tea.String(d.config.InstanceId),
RegionId: tea.String(d.config.Region),
}
describeDefaultHttpsResp, err := d.sdkClient.DescribeDefaultHttps(describeDefaultHttpsReq)
d.logger.Debug("sdk request 'waf.DescribeDefaultHttps'", slog.Any("request", describeDefaultHttpsReq), slog.Any("response", describeDefaultHttpsResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'waf.DescribeDefaultHttps': %w", err)
}
// 修改默认 SSL/TLS 设置
// REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifydefaulthttps
modifyDefaultHttpsReq := &aliwaf.ModifyDefaultHttpsRequest{
ResourceManagerResourceGroupId: xtypes.ToPtrOrZeroNil(d.config.ResourceGroupId),
InstanceId: tea.String(d.config.InstanceId),
RegionId: tea.String(d.config.Region),
CertId: tea.String(upres.CertId),
TLSVersion: tea.String("tlsv1"),
EnableTLSv3: tea.Bool(false),
}
if describeDefaultHttpsResp.Body != nil && describeDefaultHttpsResp.Body.DefaultHttps != nil {
modifyDefaultHttpsReq.TLSVersion = describeDefaultHttpsResp.Body.DefaultHttps.TLSVersion
modifyDefaultHttpsReq.EnableTLSv3 = describeDefaultHttpsResp.Body.DefaultHttps.EnableTLSv3
}
modifyDefaultHttpsResp, err := d.sdkClient.ModifyDefaultHttps(modifyDefaultHttpsReq)
d.logger.Debug("sdk request 'waf.ModifyDefaultHttps'", slog.Any("request", modifyDefaultHttpsReq), slog.Any("response", modifyDefaultHttpsResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'waf.ModifyDefaultHttps': %w", err)
}
} else {
// 指定接入域名
// 查询 CNAME 接入详情
// REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describedomaindetail
describeDomainDetailReq := &aliwaf.DescribeDomainDetailRequest{
InstanceId: tea.String(d.config.InstanceId),
RegionId: tea.String(d.config.Region),
Domain: tea.String(d.config.Domain),
}
describeDomainDetailResp, err := d.sdkClient.DescribeDomainDetail(describeDomainDetailReq)
d.logger.Debug("sdk request 'waf.DescribeDomainDetail'", slog.Any("request", describeDomainDetailReq), slog.Any("response", describeDomainDetailResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'waf.DescribeDomainDetail': %w", err)
}
// 修改 CNAME 接入资源
// REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifydomain
modifyDomainReq := &aliwaf.ModifyDomainRequest{
InstanceId: tea.String(d.config.InstanceId),
RegionId: tea.String(d.config.Region),
Domain: tea.String(d.config.Domain),
Listen: &aliwaf.ModifyDomainRequestListen{CertId: tea.String(upres.ExtendedData["certIdentifier"].(string))},
Redirect: &aliwaf.ModifyDomainRequestRedirect{Loadbalance: tea.String("iphash")},
}
modifyDomainReq = assign(modifyDomainReq, describeDomainDetailResp.Body)
modifyDomainResp, err := d.sdkClient.ModifyDomain(modifyDomainReq)
d.logger.Debug("sdk request 'waf.ModifyDomain'", slog.Any("request", modifyDomainReq), slog.Any("response", modifyDomainResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'waf.ModifyDomain': %w", err)
}
}
return nil
}
func createSDKClient(accessKeyId, accessKeySecret, region string) (*aliwaf.Client, error) {
// 接入点一览https://api.aliyun.com/product/waf-openapi
endpoint := strings.ReplaceAll(fmt.Sprintf("wafopenapi.%s.aliyuncs.com", region), "..", ".")
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String(endpoint),
}
client, err := aliwaf.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}
func createSSLManager(accessKeyId, accessKeySecret, resourceGroupId, region string) (core.SSLManager, error) {
casRegion := region
if casRegion != "" {
// 阿里云 CAS 服务接入点是独立于 WAF 服务的
// 国内版固定接入点:华东一杭州
// 国际版固定接入点:亚太东南一新加坡
if !strings.HasPrefix(casRegion, "cn-") {
casRegion = "ap-southeast-1"
} else {
casRegion = "cn-hangzhou"
}
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
ResourceGroupId: resourceGroupId,
Region: casRegion,
})
return sslmgr, err
}
func assign(source *aliwaf.ModifyDomainRequest, target *aliwaf.DescribeDomainDetailResponseBody) *aliwaf.ModifyDomainRequest {
// `ModifyDomain` 中不传的字段表示使用默认值、而非保留原值,
// 因此这里需要把原配置中的参数重新赋值回去。
if target == nil {
return source
}
if target.Listen != nil {
if source.Listen == nil {
source.Listen = &aliwaf.ModifyDomainRequestListen{}
}
if target.Listen.CipherSuite != nil {
source.Listen.CipherSuite = tea.Int32(int32(*target.Listen.CipherSuite))
}
if target.Listen.CustomCiphers != nil {
source.Listen.CustomCiphers = target.Listen.CustomCiphers
}
if target.Listen.EnableTLSv3 != nil {
source.Listen.EnableTLSv3 = target.Listen.EnableTLSv3
}
if target.Listen.ExclusiveIp != nil {
source.Listen.ExclusiveIp = target.Listen.ExclusiveIp
}
if target.Listen.FocusHttps != nil {
source.Listen.FocusHttps = target.Listen.FocusHttps
}
if target.Listen.Http2Enabled != nil {
source.Listen.Http2Enabled = target.Listen.Http2Enabled
}
if target.Listen.HttpPorts != nil {
source.Listen.HttpPorts = xslices.Map(target.Listen.HttpPorts, func(v *int64) *int32 {
if v == nil {
return nil
}
return tea.Int32(int32(*v))
})
}
if target.Listen.HttpsPorts != nil {
source.Listen.HttpsPorts = xslices.Map(target.Listen.HttpsPorts, func(v *int64) *int32 {
if v == nil {
return nil
}
return tea.Int32(int32(*v))
})
}
if target.Listen.IPv6Enabled != nil {
source.Listen.IPv6Enabled = target.Listen.IPv6Enabled
}
if target.Listen.ProtectionResource != nil {
source.Listen.ProtectionResource = target.Listen.ProtectionResource
}
if target.Listen.TLSVersion != nil {
source.Listen.TLSVersion = target.Listen.TLSVersion
}
if target.Listen.XffHeaderMode != nil {
source.Listen.XffHeaderMode = tea.Int32(int32(*target.Listen.XffHeaderMode))
}
if target.Listen.XffHeaders != nil {
source.Listen.XffHeaders = target.Listen.XffHeaders
}
}
if target.Redirect != nil {
if source.Redirect == nil {
source.Redirect = &aliwaf.ModifyDomainRequestRedirect{}
}
if target.Redirect.Backends != nil {
source.Redirect.Backends = xslices.Map(target.Redirect.Backends, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectBackends) *string {
if v == nil {
return nil
}
return v.Backend
})
}
if target.Redirect.BackupBackends != nil {
source.Redirect.BackupBackends = xslices.Map(target.Redirect.BackupBackends, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectBackupBackends) *string {
if v == nil {
return nil
}
return v.Backend
})
}
if target.Redirect.ConnectTimeout != nil {
source.Redirect.ConnectTimeout = target.Redirect.ConnectTimeout
}
if target.Redirect.FocusHttpBackend != nil {
source.Redirect.FocusHttpBackend = target.Redirect.FocusHttpBackend
}
if target.Redirect.Keepalive != nil {
source.Redirect.Keepalive = target.Redirect.Keepalive
}
if target.Redirect.KeepaliveRequests != nil {
source.Redirect.KeepaliveRequests = target.Redirect.KeepaliveRequests
}
if target.Redirect.KeepaliveTimeout != nil {
source.Redirect.KeepaliveTimeout = target.Redirect.KeepaliveTimeout
}
if target.Redirect.Loadbalance != nil {
source.Redirect.Loadbalance = target.Redirect.Loadbalance
}
if target.Redirect.ReadTimeout != nil {
source.Redirect.ReadTimeout = target.Redirect.ReadTimeout
}
if target.Redirect.RequestHeaders != nil {
source.Redirect.RequestHeaders = xslices.Map(target.Redirect.RequestHeaders, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectRequestHeaders) *aliwaf.ModifyDomainRequestRedirectRequestHeaders {
if v == nil {
return nil
}
return &aliwaf.ModifyDomainRequestRedirectRequestHeaders{
Key: v.Key,
Value: v.Value,
}
})
}
if target.Redirect.Retry != nil {
source.Redirect.Retry = target.Redirect.Retry
}
if target.Redirect.SniEnabled != nil {
source.Redirect.SniEnabled = target.Redirect.SniEnabled
}
if target.Redirect.SniHost != nil {
source.Redirect.SniHost = target.Redirect.SniHost
}
if target.Redirect.WriteTimeout != nil {
source.Redirect.WriteTimeout = target.Redirect.WriteTimeout
}
if target.Redirect.XffProto != nil {
source.Redirect.XffProto = target.Redirect.XffProto
}
}
return source
}

View File

@@ -0,0 +1,80 @@
package aliyunwaf_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aliyun-waf"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fRegion string
fInstanceId string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_ALIYUNWAF_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fInstanceId, argsPrefix+"INSTANCEID", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_waf_test.go -args \
--CERTIMATE_SSLDEPLOYER_ALIYUNWAF_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNWAF_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_ALIYUNWAF_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_ALIYUNWAF_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_SSLDEPLOYER_ALIYUNWAF_REGION="cn-hangzhou" \
--CERTIMATE_SSLDEPLOYER_ALIYUNWAF_INSTANCEID="your-waf-instance-id"
*/
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),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("INSTANCEID: %v", fInstanceId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
InstanceId: fInstanceId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,118 @@
package apisix
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
apisixsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/apisix"
xcert "github.com/usual2970/certimate/internal/pkg/utils/cert"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// APISIX 服务地址。
ServerUrl string `json:"serverUrl"`
// APISIX Admin API Key。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 证书 ID。
// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。
CertificateId string `json:"certificateId,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *apisixsdk.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.CertificateId == "" {
return errors.New("config `certificateId` is required")
}
// 解析证书内容
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return err
}
// 更新 SSL 证书
// REF: https://apisix.apache.org/zh/docs/apisix/admin-api/#ssl
updateSSLReq := &apisixsdk.UpdateSSLRequest{
Cert: xtypes.ToPtr(certPEM),
Key: xtypes.ToPtr(privkeyPEM),
SNIs: xtypes.ToPtr(certX509.DNSNames),
Type: xtypes.ToPtr("server"),
Status: xtypes.ToPtr(int32(1)),
}
updateSSLResp, err := d.sdkClient.UpdateSSL(d.config.CertificateId, updateSSLReq)
d.logger.Debug("sdk request 'apisix.UpdateSSL'", slog.String("sslId", d.config.CertificateId), slog.Any("request", updateSSLReq), slog.Any("response", updateSSLResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'apisix.UpdateSSL': %w", err)
}
return nil
}
func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*apisixsdk.Client, error) {
client, err := apisixsdk.NewClient(serverUrl, apiKey)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,77 @@
package apisix_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/apisix"
)
var (
fInputCertPath string
fInputKeyPath string
fServerUrl string
fApiKey string
fCertificateId string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_APISIX_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
}
/*
Shell command to run this test:
go test -v ./apisix_test.go -args \
--CERTIMATE_SSLDEPLOYER_APISIX_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_APISIX_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_APISIX_SERVERURL="http://127.0.0.1:9080" \
--CERTIMATE_SSLDEPLOYER_APISIX_APIKEY="your-api-key" \
--CERTIMATE_SSLDEPLOYER_APISIX_CERTIFICATEID="your-cerficiate-id"
*/
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("SERVERURL: %v", fServerUrl),
fmt.Sprintf("APIKEY: %v", fApiKey),
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ServerUrl: fServerUrl,
ApiKey: fApiKey,
AllowInsecureConnections: true,
ResourceType: provider.RESOURCE_TYPE_CERTIFICATE,
CertificateId: fCertificateId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,8 @@
package apisix
type ResourceType string
const (
// 资源类型:替换指定证书。
RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate")
)

View File

@@ -0,0 +1,122 @@
package awsacm
import (
"context"
"errors"
"fmt"
"log/slog"
aws "github.com/aws/aws-sdk-go-v2/aws"
awscfg "github.com/aws/aws-sdk-go-v2/config"
awscred "github.com/aws/aws-sdk-go-v2/credentials"
awsacm "github.com/aws/aws-sdk-go-v2/service/acm"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aws-acm"
xcert "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type SSLDeployerProviderConfig struct {
// AWS AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// AWS SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// AWS 区域。
Region string `json:"region"`
// ACM 证书 ARN。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateArn string `json:"certificateArn,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *awsacm.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
Region: config.Region,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.CertificateArn == "" {
// 上传证书
upres, err := d.sslManager.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))
}
} else {
// 提取服务器证书
serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 导入证书
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html
importCertificateReq := &awsacm.ImportCertificateInput{
CertificateArn: aws.String(d.config.CertificateArn),
Certificate: ([]byte)(serverCertPEM),
CertificateChain: ([]byte)(intermediaCertPEM),
PrivateKey: ([]byte)(privkeyPEM),
}
importCertificateResp, err := d.sdkClient.ImportCertificate(context.TODO(), importCertificateReq)
d.logger.Debug("sdk request 'acm.ImportCertificate'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'acm.ImportCertificate': %w", err)
}
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, secretAccessKey, region string) (*awsacm.Client, error) {
cfg, err := awscfg.LoadDefaultConfig(context.TODO())
if err != nil {
return nil, err
}
client := awsacm.NewFromConfig(cfg, func(o *awsacm.Options) {
o.Region = region
o.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, ""))
})
return client, nil
}

View File

@@ -0,0 +1,163 @@
package awscloudfront
import (
"context"
"errors"
"fmt"
"log/slog"
aws "github.com/aws/aws-sdk-go-v2/aws"
awscfg "github.com/aws/aws-sdk-go-v2/config"
awscred "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/cloudfront"
"github.com/aws/aws-sdk-go-v2/service/cloudfront/types"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrspacm "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aws-acm"
sslmgrspiam "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aws-iam"
)
type SSLDeployerProviderConfig struct {
// AWS AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// AWS SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// AWS 区域。
Region string `json:"region"`
// AWS CloudFront 分配 ID。
DistributionId string `json:"distributionId"`
// AWS CloudFront 证书来源。
// 可取值 "ACM"、"IAM"。
CertificateSource string `json:"certificateSource"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *cloudfront.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
var sslmgr core.SSLManager
if config.CertificateSource == "ACM" {
sslmgr, err = sslmgrspacm.NewSSLManagerProvider(&sslmgrspacm.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
Region: config.Region,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
} else if config.CertificateSource == "IAM" {
sslmgr, err = sslmgrspiam.NewSSLManagerProvider(&sslmgrspiam.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
Region: config.Region,
CertificatePath: "/cloudfront/",
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
} else {
return nil, fmt.Errorf("unsupported certificate source: '%s'", config.CertificateSource)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.DistributionId == "" {
return nil, errors.New("config `distribuitionId` is required")
}
// 上传证书
upres, err := d.sslManager.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))
}
// 获取分配配置
// REF: https://docs.aws.amazon.com/en_us/cloudfront/latest/APIReference/API_GetDistributionConfig.html
getDistributionConfigReq := &cloudfront.GetDistributionConfigInput{
Id: aws.String(d.config.DistributionId),
}
getDistributionConfigResp, err := d.sdkClient.GetDistributionConfig(context.TODO(), getDistributionConfigReq)
d.logger.Debug("sdk request 'cloudfront.GetDistributionConfig'", slog.Any("request", getDistributionConfigReq), slog.Any("response", getDistributionConfigResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cloudfront.GetDistributionConfig': %w", err)
}
// 更新分配配置
// REF: https://docs.aws.amazon.com/zh_cn/cloudfront/latest/APIReference/API_UpdateDistribution.html
updateDistributionReq := &cloudfront.UpdateDistributionInput{
Id: aws.String(d.config.DistributionId),
DistributionConfig: getDistributionConfigResp.DistributionConfig,
IfMatch: getDistributionConfigResp.ETag,
}
if updateDistributionReq.DistributionConfig.ViewerCertificate == nil {
updateDistributionReq.DistributionConfig.ViewerCertificate = &types.ViewerCertificate{}
}
updateDistributionReq.DistributionConfig.ViewerCertificate.CloudFrontDefaultCertificate = aws.Bool(false)
if d.config.CertificateSource == "ACM" {
updateDistributionReq.DistributionConfig.ViewerCertificate.ACMCertificateArn = aws.String(upres.CertId)
updateDistributionReq.DistributionConfig.ViewerCertificate.IAMCertificateId = nil
} else if d.config.CertificateSource == "IAM" {
updateDistributionReq.DistributionConfig.ViewerCertificate.ACMCertificateArn = nil
updateDistributionReq.DistributionConfig.ViewerCertificate.IAMCertificateId = aws.String(upres.CertId)
if updateDistributionReq.DistributionConfig.ViewerCertificate.MinimumProtocolVersion == "" {
updateDistributionReq.DistributionConfig.ViewerCertificate.MinimumProtocolVersion = types.MinimumProtocolVersionTLSv1
}
if updateDistributionReq.DistributionConfig.ViewerCertificate.SSLSupportMethod == "" {
updateDistributionReq.DistributionConfig.ViewerCertificate.SSLSupportMethod = types.SSLSupportMethodSniOnly
}
}
updateDistributionResp, err := d.sdkClient.UpdateDistribution(context.TODO(), updateDistributionReq)
d.logger.Debug("sdk request 'cloudfront.UpdateDistribution'", slog.Any("request", updateDistributionReq), slog.Any("response", updateDistributionResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cloudfront.UpdateDistribution': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, secretAccessKey, region string) (*cloudfront.Client, error) {
cfg, err := awscfg.LoadDefaultConfig(context.TODO())
if err != nil {
return nil, err
}
client := cloudfront.NewFromConfig(cfg, func(o *cloudfront.Options) {
o.Region = region
o.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, ""))
})
return client, nil
}

View File

@@ -0,0 +1,80 @@
package awscloudfront_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/aws-cloudfront"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegion string
fDistribuitionId string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_AWSCLOUDFRONT_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fDistribuitionId, argsPrefix+"DISTRIBUTIONID", "", "")
}
/*
Shell command to run this test:
go test -v ./aws_cloudfront_test.go -args \
--CERTIMATE_SSLDEPLOYER_AWSCLOUDFRONT_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_AWSCLOUDFRONT_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_AWSCLOUDFRONT_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_AWSCLOUDFRONT_SECRETACCESSKEY="your-secret-access-id" \
--CERTIMATE_SSLDEPLOYER_AWSCLOUDFRONT_REGION="us-east-1" \
--CERTIMATE_SSLDEPLOYER_AWSCLOUDFRONT_DISTRIBUTIONID="your-distribution-id"
*/
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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("DISTRIBUTIONID: %v", fDistribuitionId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
DistributionId: fDistribuitionId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,75 @@
package awsiam
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/aws-iam"
)
type SSLDeployerProviderConfig struct {
// AWS AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// AWS SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// AWS 区域。
Region string `json:"region"`
// IAM 证书路径。
// 选填。
CertificatePath string `json:"certificatePath,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
Region: config.Region,
CertificatePath: config.CertificatePath,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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))
}
return &core.SSLDeployResult{}, nil
}

View File

@@ -0,0 +1,176 @@
package azurekeyvault
import (
"context"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/azure-keyvault"
azenv "github.com/usual2970/certimate/internal/pkg/sdk3rd/azure/env"
xcert "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type SSLDeployerProviderConfig struct {
// Azure TenantId。
TenantId string `json:"tenantId"`
// Azure ClientId。
ClientId string `json:"clientId"`
// Azure ClientSecret。
ClientSecret string `json:"clientSecret"`
// Azure 主权云环境。
CloudName string `json:"cloudName,omitempty"`
// Key Vault 名称。
KeyVaultName string `json:"keyvaultName"`
// Key Vault 证书名称。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateName string `json:"certificateName,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *azcertificates.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.TenantId, config.ClientId, config.ClientSecret, config.CloudName, config.KeyVaultName)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
TenantId: config.TenantId,
ClientId: config.ClientId,
ClientSecret: config.ClientSecret,
CloudName: config.CloudName,
KeyVaultName: config.KeyVaultName,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 解析证书内容
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 转换证书格式
certPFX, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, "")
if err != nil {
return nil, fmt.Errorf("failed to transform certificate from PEM to PFX: %w", err)
}
if d.config.CertificateName == "" {
// 上传证书
upres, err := d.sslManager.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))
}
} else {
// 获取证书
// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/get-certificate/get-certificate
getCertificateResp, err := d.sdkClient.GetCertificate(context.TODO(), d.config.CertificateName, "", nil)
d.logger.Debug("sdk request 'keyvault.GetCertificate'", slog.String("request.certificateName", d.config.CertificateName), slog.Any("response", getCertificateResp))
if err != nil {
var respErr *azcore.ResponseError
if !errors.As(err, &respErr) || (respErr.ErrorCode != "ResourceNotFound" && respErr.ErrorCode != "CertificateNotFound") {
return nil, fmt.Errorf("failed to execute sdk request 'keyvault.GetCertificate': %w", err)
}
} else {
oldCertX509, err := x509.ParseCertificate(getCertificateResp.CER)
if err == nil {
if xcert.EqualCertificate(certX509, oldCertX509) {
return &core.SSLDeployResult{}, nil
}
}
}
// 导入证书
// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/import-certificate/import-certificate
importCertificateParams := azcertificates.ImportCertificateParameters{
Base64EncodedCertificate: to.Ptr(base64.StdEncoding.EncodeToString(certPFX)),
CertificatePolicy: &azcertificates.CertificatePolicy{
SecretProperties: &azcertificates.SecretProperties{
ContentType: to.Ptr("application/x-pkcs12"),
},
},
Tags: map[string]*string{
"certimate/cert-cn": to.Ptr(certX509.Subject.CommonName),
"certimate/cert-sn": to.Ptr(certX509.SerialNumber.Text(16)),
},
}
importCertificateResp, err := d.sdkClient.ImportCertificate(context.TODO(), d.config.CertificateName, importCertificateParams, nil)
d.logger.Debug("sdk request 'keyvault.ImportCertificate'", slog.String("request.certificateName", d.config.CertificateName), slog.Any("request.parameters", importCertificateParams), slog.Any("response", importCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'keyvault.ImportCertificate': %w", err)
}
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(tenantId, clientId, clientSecret, cloudName, keyvaultName string) (*azcertificates.Client, error) {
env, err := azenv.GetCloudEnvConfiguration(cloudName)
if err != nil {
return nil, err
}
clientOptions := azcore.ClientOptions{Cloud: env}
credential, err := azidentity.NewClientSecretCredential(tenantId, clientId, clientSecret,
&azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions})
if err != nil {
return nil, err
}
endpoint := fmt.Sprintf("https://%s.vault.azure.net", keyvaultName)
if azenv.IsUSGovernmentEnv(cloudName) {
endpoint = fmt.Sprintf("https://%s.vault.usgovcloudapi.net", keyvaultName)
} else if azenv.IsChinaEnv(cloudName) {
endpoint = fmt.Sprintf("https://%s.vault.azure.cn", keyvaultName)
}
client, err := azcertificates.NewClient(endpoint, credential, nil)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,344 @@
package baiducloudappblb
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
bceappblb "github.com/baidubce/bce-sdk-go/services/appblb"
"github.com/google/uuid"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/baiducloud-cert"
xslices "github.com/usual2970/certimate/internal/pkg/utils/slices"
)
type SSLDeployerProviderConfig struct {
// 百度智能云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 百度智能云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 百度智能云区域。
Region string `json:"region"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听端口。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerPort int32 `json:"listenerPort,omitempty"`
// SNI 域名(支持泛域名)。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。
Domain string `json:"domain,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *bceappblb.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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))
}
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case RESOURCE_TYPE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
// 查询 BLB 实例详情
// REF: https://cloud.baidu.com/doc/BLB/s/6jwvxnyhi#describeloadbalancerdetail%E6%9F%A5%E8%AF%A2blb%E5%AE%9E%E4%BE%8B%E8%AF%A6%E6%83%85
describeLoadBalancerDetailResp, err := d.sdkClient.DescribeLoadBalancerDetail(d.config.LoadbalancerId)
d.logger.Debug("sdk request 'appblb.DescribeLoadBalancerAttribute'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("response", describeLoadBalancerDetailResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'appblb.DescribeLoadBalancerDetail': %w", err)
}
// 获取全部 HTTPS/SSL 监听端口
listeners := make([]struct {
Type string
Port int32
}, 0)
for _, listener := range describeLoadBalancerDetailResp.Listener {
if listener.Type == "HTTPS" || listener.Type == "SSL" {
listenerPort, err := strconv.Atoi(listener.Port)
if err != nil {
continue
}
listeners = append(listeners, struct {
Type string
Port int32
}{
Type: listener.Type,
Port: int32(listenerPort),
})
}
}
// 遍历更新监听证书
if len(listeners) == 0 {
d.logger.Info("no blb listeners to deploy")
} else {
d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners))
var errs []error
for _, listener := range listeners {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *SSLDeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerPort == 0 {
return errors.New("config `listenerPort` is required")
}
// 查询监听
// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#describeappalllisteners%E6%9F%A5%E8%AF%A2%E6%89%80%E6%9C%89%E7%9B%91%E5%90%AC
describeAppAllListenersRequest := &bceappblb.DescribeAppListenerArgs{
ListenerPort: uint16(d.config.ListenerPort),
}
describeAppAllListenersResp, err := d.sdkClient.DescribeAppAllListeners(d.config.LoadbalancerId, describeAppAllListenersRequest)
d.logger.Debug("sdk request 'appblb.DescribeAppAllListeners'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("request", describeAppAllListenersRequest), slog.Any("response", describeAppAllListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'appblb.DescribeAppAllListeners': %w", err)
}
// 获取全部 HTTPS/SSL 监听端口
listeners := make([]struct {
Type string
Port int32
}, 0)
for _, listener := range describeAppAllListenersResp.ListenerList {
if listener.ListenerType == "HTTPS" || listener.ListenerType == "SSL" {
listeners = append(listeners, struct {
Type string
Port int32
}{
Type: listener.ListenerType,
Port: int32(listener.ListenerPort),
})
}
}
// 遍历更新监听证书
if len(listeners) == 0 {
d.logger.Info("no blb listeners to deploy")
} else {
d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners))
var errs []error
for _, listener := range listeners {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *SSLDeployerProvider) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerType string, cloudListenerPort int32, cloudCertId string) error {
switch strings.ToUpper(cloudListenerType) {
case "HTTPS":
return d.updateHttpsListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)
case "SSL":
return d.updateSslListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)
default:
return fmt.Errorf("unsupported listener type '%s'", cloudListenerType)
}
}
func (d *SSLDeployerProvider) updateHttpsListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {
// 查询 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#describeapphttpslisteners%E6%9F%A5%E8%AF%A2https%E7%9B%91%E5%90%AC%E5%99%A8
describeAppHTTPSListenersReq := &bceappblb.DescribeAppListenerArgs{
ListenerPort: uint16(cloudHttpsListenerPort),
MaxKeys: 1,
}
describeAppHTTPSListenersResp, err := d.sdkClient.DescribeAppHTTPSListeners(cloudLoadbalancerId, describeAppHTTPSListenersReq)
d.logger.Debug("sdk request 'appblb.DescribeAppHTTPSListeners'", slog.String("blbId", cloudLoadbalancerId), slog.Any("request", describeAppHTTPSListenersReq), slog.Any("response", describeAppHTTPSListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'appblb.DescribeAppHTTPSListeners': %w", err)
} else if len(describeAppHTTPSListenersResp.ListenerList) == 0 {
return fmt.Errorf("listener %s:%d not found", cloudLoadbalancerId, cloudHttpsListenerPort)
}
if d.config.Domain == "" {
// 未指定 SNI只需部署到监听器
// 更新 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#updateapphttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8
updateAppHTTPSListenerReq := &bceappblb.UpdateAppHTTPSListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
Scheduler: describeAppHTTPSListenersResp.ListenerList[0].Scheduler,
CertIds: []string{cloudCertId},
}
err := d.sdkClient.UpdateAppHTTPSListener(cloudLoadbalancerId, updateAppHTTPSListenerReq)
d.logger.Debug("sdk request 'appblb.UpdateAppHTTPSListener'", slog.Any("request", updateAppHTTPSListenerReq))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'appblb.UpdateAppHTTPSListener': %w", err)
}
} else {
// 指定 SNI需部署到扩展域名
// 更新 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8
updateAppHTTPSListenerReq := &bceappblb.UpdateAppHTTPSListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
Scheduler: describeAppHTTPSListenersResp.ListenerList[0].Scheduler,
CertIds: describeAppHTTPSListenersResp.ListenerList[0].CertIds,
AdditionalCertDomains: xslices.Map(describeAppHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceappblb.AdditionalCertDomainsModel) bceappblb.AdditionalCertDomainsModel {
if domain.Host == d.config.Domain {
return bceappblb.AdditionalCertDomainsModel{
Host: domain.Host,
CertId: cloudCertId,
}
}
return bceappblb.AdditionalCertDomainsModel{
Host: domain.Host,
CertId: domain.CertId,
}
}),
}
err := d.sdkClient.UpdateAppHTTPSListener(cloudLoadbalancerId, updateAppHTTPSListenerReq)
d.logger.Debug("sdk request 'appblb.UpdateAppHTTPSListener'", slog.Any("request", updateAppHTTPSListenerReq))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'appblb.UpdateAppHTTPSListener': %w", err)
}
}
return nil
}
func (d *SSLDeployerProvider) updateSslListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {
// 更新 SSL 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#updateappssllistener%E6%9B%B4%E6%96%B0ssl%E7%9B%91%E5%90%AC%E5%99%A8
updateAppSSLListenerReq := &bceappblb.UpdateAppSSLListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
CertIds: []string{cloudCertId},
}
err := d.sdkClient.UpdateAppSSLListener(cloudLoadbalancerId, updateAppSSLListenerReq)
d.logger.Debug("sdk request 'appblb.UpdateAppSSLListener'", slog.Any("request", updateAppSSLListenerReq))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'appblb.UpdateAppSSLListener': %w", err)
}
return nil
}
func createSDKClient(accessKeyId, secretAccessKey, region string) (*bceappblb.Client, error) {
endpoint := ""
if region != "" {
endpoint = fmt.Sprintf("blb.%s.baidubce.com", region)
}
client, err := bceappblb.NewClient(accessKeyId, secretAccessKey, endpoint)
if err != nil {
return nil, err
}
return client, nil
}
func generateClientToken() string {
return strings.ReplaceAll(uuid.New().String(), "-", "")
}

View File

@@ -0,0 +1,86 @@
package baiducloudappblb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/baiducloud-appblb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegion string
fLoadbalancerId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_BAIDUCLOUDAPPBLB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./baiducloud_appblb_test.go -args \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDAPPBLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDAPPBLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDAPPBLB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDAPPBLB_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDAPPBLB_REGION="bj" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDAPPBLB_LOADBALANCERID="your-blb-loadbalancer-id" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDAPPBLB_DOMAIN="your-blb-sni-domain"
*/
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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
ResourceType: provider.RESOURCE_TYPE_LOADBALANCER,
Region: fRegion,
LoadbalancerId: fLoadbalancerId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,10 @@
package baiducloudappblb
type ResourceType string
const (
// 资源类型:部署到指定负载均衡器。
RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@@ -0,0 +1,342 @@
package baiducloudblb
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
bceblb "github.com/baidubce/bce-sdk-go/services/blb"
"github.com/google/uuid"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/baiducloud-cert"
xslices "github.com/usual2970/certimate/internal/pkg/utils/slices"
)
type SSLDeployerProviderConfig struct {
// 百度智能云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 百度智能云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 百度智能云区域。
Region string `json:"region"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听端口。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerPort int32 `json:"listenerPort,omitempty"`
// SNI 域名(支持泛域名)。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。
Domain string `json:"domain,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *bceblb.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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))
}
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case RESOURCE_TYPE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
// 查询 BLB 实例详情
// REF: https://cloud.baidu.com/doc/BLB/s/njwvxnv79#describeloadbalancerdetail%E6%9F%A5%E8%AF%A2blb%E5%AE%9E%E4%BE%8B%E8%AF%A6%E6%83%85
describeLoadBalancerDetailResp, err := d.sdkClient.DescribeLoadBalancerDetail(d.config.LoadbalancerId)
d.logger.Debug("sdk request 'blb.DescribeLoadBalancerAttribute'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("response", describeLoadBalancerDetailResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'blb.DescribeLoadBalancerDetail': %w", err)
}
// 获取全部 HTTPS/SSL 监听端口
listeners := make([]struct {
Type string
Port int32
}, 0)
for _, listener := range describeLoadBalancerDetailResp.Listener {
if listener.Type == "HTTPS" || listener.Type == "SSL" {
listenerPort, err := strconv.Atoi(listener.Port)
if err != nil {
continue
}
listeners = append(listeners, struct {
Type string
Port int32
}{
Type: listener.Type,
Port: int32(listenerPort),
})
}
}
// 遍历更新监听证书
if len(listeners) == 0 {
d.logger.Info("no blb listeners to deploy")
} else {
d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners))
var errs []error
for _, listener := range listeners {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *SSLDeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerPort == 0 {
return errors.New("config `listenerPort` is required")
}
// 查询监听
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#describealllisteners%E6%9F%A5%E8%AF%A2%E6%89%80%E6%9C%89%E7%9B%91%E5%90%AC
describeAllListenersRequest := &bceblb.DescribeListenerArgs{
ListenerPort: uint16(d.config.ListenerPort),
}
describeAllListenersResp, err := d.sdkClient.DescribeAllListeners(d.config.LoadbalancerId, describeAllListenersRequest)
d.logger.Debug("sdk request 'blb.DescribeAllListeners'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("request", describeAllListenersRequest), slog.Any("response", describeAllListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'blb.DescribeAllListeners': %w", err)
}
// 获取全部 HTTPS/SSL 监听端口
listeners := make([]struct {
Type string
Port int32
}, 0)
for _, listener := range describeAllListenersResp.AllListenerList {
if listener.ListenerType == "HTTPS" || listener.ListenerType == "SSL" {
listeners = append(listeners, struct {
Type string
Port int32
}{
Type: listener.ListenerType,
Port: int32(listener.ListenerPort),
})
}
}
// 遍历更新监听证书
if len(listeners) == 0 {
d.logger.Info("no blb listeners to deploy")
} else {
d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners))
var errs []error
for _, listener := range listeners {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *SSLDeployerProvider) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerType string, cloudListenerPort int32, cloudCertId string) error {
switch strings.ToUpper(cloudListenerType) {
case "HTTPS":
return d.updateHttpsListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)
case "SSL":
return d.updateSslListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)
default:
return fmt.Errorf("unsupported listener type '%s'", cloudListenerType)
}
}
func (d *SSLDeployerProvider) updateHttpsListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {
// 查询 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#describehttpslisteners%E6%9F%A5%E8%AF%A2https%E7%9B%91%E5%90%AC%E5%99%A8
describeHTTPSListenersReq := &bceblb.DescribeListenerArgs{
ListenerPort: uint16(cloudHttpsListenerPort),
MaxKeys: 1,
}
describeHTTPSListenersResp, err := d.sdkClient.DescribeHTTPSListeners(cloudLoadbalancerId, describeHTTPSListenersReq)
d.logger.Debug("sdk request 'blb.DescribeHTTPSListeners'", slog.String("blbId", cloudLoadbalancerId), slog.Any("request", describeHTTPSListenersReq), slog.Any("response", describeHTTPSListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'blb.DescribeHTTPSListeners': %w", err)
} else if len(describeHTTPSListenersResp.ListenerList) == 0 {
return fmt.Errorf("listener %s:%d not found", cloudLoadbalancerId, cloudHttpsListenerPort)
}
if d.config.Domain == "" {
// 未指定 SNI只需部署到监听器
// 更新 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8
updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
CertIds: []string{cloudCertId},
}
err := d.sdkClient.UpdateHTTPSListener(cloudLoadbalancerId, updateHTTPSListenerReq)
d.logger.Debug("sdk request 'blb.UpdateHTTPSListener'", slog.Any("request", updateHTTPSListenerReq))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'blb.UpdateHTTPSListener': %w", err)
}
} else {
// 指定 SNI需部署到扩展域名
// 更新 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8
updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
CertIds: describeHTTPSListenersResp.ListenerList[0].CertIds,
AdditionalCertDomains: xslices.Map(describeHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceblb.AdditionalCertDomainsModel) bceblb.AdditionalCertDomainsModel {
if domain.Host == d.config.Domain {
return bceblb.AdditionalCertDomainsModel{
Host: domain.Host,
CertId: cloudCertId,
}
}
return bceblb.AdditionalCertDomainsModel{
Host: domain.Host,
CertId: domain.CertId,
}
}),
}
err := d.sdkClient.UpdateHTTPSListener(cloudLoadbalancerId, updateHTTPSListenerReq)
d.logger.Debug("sdk request 'blb.UpdateHTTPSListener'", slog.Any("request", updateHTTPSListenerReq))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'blb.UpdateHTTPSListener': %w", err)
}
}
return nil
}
func (d *SSLDeployerProvider) updateSslListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {
// 更新 SSL 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatessllistener%E6%9B%B4%E6%96%B0ssl%E7%9B%91%E5%90%AC%E5%99%A8
updateSSLListenerReq := &bceblb.UpdateSSLListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
CertIds: []string{cloudCertId},
}
err := d.sdkClient.UpdateSSLListener(cloudLoadbalancerId, updateSSLListenerReq)
d.logger.Debug("sdk request 'blb.UpdateSSLListener'", slog.Any("request", updateSSLListenerReq))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'blb.UpdateSSLListener': %w", err)
}
return nil
}
func createSDKClient(accessKeyId, secretAccessKey, region string) (*bceblb.Client, error) {
endpoint := ""
if region != "" {
endpoint = fmt.Sprintf("blb.%s.baidubce.com", region)
}
client, err := bceblb.NewClient(accessKeyId, secretAccessKey, endpoint)
if err != nil {
return nil, err
}
return client, nil
}
func generateClientToken() string {
return strings.ReplaceAll(uuid.New().String(), "-", "")
}

View File

@@ -0,0 +1,86 @@
package baiducloudblb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/baiducloud-blb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegion string
fLoadbalancerId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_BAIDUCLOUDBLB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./baiducloud_blb_test.go -args \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDBLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDBLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDBLB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDBLB_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDBLB_REGION="bj" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDBLB_LOADBALANCERID="your-blb-loadbalancer-id" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDBLB_DOMAIN="your-blb-sni-domain"
*/
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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
ResourceType: provider.RESOURCE_TYPE_LOADBALANCER,
Region: fRegion,
LoadbalancerId: fLoadbalancerId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,10 @@
package baiducloudblb
type ResourceType string
const (
// 资源类型:部署到指定负载均衡器。
RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@@ -0,0 +1,88 @@
package baiducloudcdn
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
bcecdn "github.com/baidubce/bce-sdk-go/services/cdn"
bcecdnapi "github.com/baidubce/bce-sdk-go/services/cdn/api"
"github.com/usual2970/certimate/internal/pkg/core"
)
type SSLDeployerProviderConfig struct {
// 百度智能云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 百度智能云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *bcecdn.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 修改域名证书
// REF: https://cloud.baidu.com/doc/CDN/s/qjzuz2hp8
putCertResp, err := d.sdkClient.PutCert(
d.config.Domain,
&bcecdnapi.UserCertificate{
CertName: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
ServerData: certPEM,
PrivateData: privkeyPEM,
},
"ON",
)
d.logger.Debug("sdk request 'cdn.PutCert'", slog.String("request.domain", d.config.Domain), slog.Any("response", putCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.PutCert': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, secretAccessKey string) (*bcecdn.Client, error) {
client, err := bcecdn.NewClient(accessKeyId, secretAccessKey, "")
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,75 @@
package baiducloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/baiducloud-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_BAIDUCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./baiducloud_cdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_SSLDEPLOYER_BAIDUCLOUDCDN_DOMAIN="example.com"
*/
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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,66 @@
package baiducloudcert
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/baiducloud-cert"
)
type SSLDeployerProviderConfig struct {
// 百度智能云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 百度智能云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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))
}
return &core.SSLDeployResult{}, nil
}

View File

@@ -0,0 +1,147 @@
package baishancdn
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"regexp"
"strings"
"time"
"github.com/usual2970/certimate/internal/pkg/core"
bssdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/baishan"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// 白山云 API Token。
ApiToken string `json:"apiToken"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
// 证书 ID。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateId string `json:"certificateId,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *bssdk.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ApiToken)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 如果原证书 ID 为空,则新增证书;否则替换证书。
if d.config.CertificateId == "" {
// 新增证书
// REF: https://portal.baishancloud.com/track/document/downloadPdf/1441
certificateId := ""
setDomainCertificateReq := &bssdk.SetDomainCertificateRequest{
Name: xtypes.ToPtr(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
Certificate: xtypes.ToPtr(certPEM),
Key: xtypes.ToPtr(privkeyPEM),
}
setDomainCertificateResp, err := d.sdkClient.SetDomainCertificate(setDomainCertificateReq)
d.logger.Debug("sdk request 'baishan.SetDomainCertificate'", slog.Any("request", setDomainCertificateReq), slog.Any("response", setDomainCertificateResp))
if err != nil {
if setDomainCertificateResp != nil {
if setDomainCertificateResp.GetCode() == 400699 && strings.Contains(setDomainCertificateResp.GetMessage(), "this certificate is exists") {
// 证书已存在,忽略新增证书接口错误
re := regexp.MustCompile(`\d+`)
certificateId = re.FindString(setDomainCertificateResp.GetMessage())
}
}
if certificateId == "" {
return nil, fmt.Errorf("failed to execute sdk request 'baishan.SetDomainCertificate': %w", err)
}
} else {
certificateId = setDomainCertificateResp.Data.CertId.String()
}
// 查询域名配置
// REF: https://portal.baishancloud.com/track/document/api/1/1065
getDomainConfigReq := &bssdk.GetDomainConfigRequest{
Domains: xtypes.ToPtr(d.config.Domain),
Config: xtypes.ToPtr([]string{"https"}),
}
getDomainConfigResp, err := d.sdkClient.GetDomainConfig(getDomainConfigReq)
d.logger.Debug("sdk request 'baishan.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'baishan.GetDomainConfig': %w", err)
} else if len(getDomainConfigResp.Data) == 0 {
return nil, errors.New("domain config not found")
}
// 设置域名配置
// REF: https://portal.baishancloud.com/track/document/api/1/1045
setDomainConfigReq := &bssdk.SetDomainConfigRequest{
Domains: xtypes.ToPtr(d.config.Domain),
Config: &bssdk.DomainConfig{
Https: &bssdk.DomainConfigHttps{
CertId: json.Number(certificateId),
ForceHttps: getDomainConfigResp.Data[0].Config.Https.ForceHttps,
EnableHttp2: getDomainConfigResp.Data[0].Config.Https.EnableHttp2,
EnableOcsp: getDomainConfigResp.Data[0].Config.Https.EnableOcsp,
},
},
}
setDomainConfigResp, err := d.sdkClient.SetDomainConfig(setDomainConfigReq)
d.logger.Debug("sdk request 'baishan.SetDomainConfig'", slog.Any("request", setDomainConfigReq), slog.Any("response", setDomainConfigResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'baishan.SetDomainConfig': %w", err)
}
} else {
// 替换证书
// REF: https://portal.baishancloud.com/track/document/downloadPdf/1441
setDomainCertificateReq := &bssdk.SetDomainCertificateRequest{
CertificateId: &d.config.CertificateId,
Name: xtypes.ToPtr(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
Certificate: xtypes.ToPtr(certPEM),
Key: xtypes.ToPtr(privkeyPEM),
}
setDomainCertificateResp, err := d.sdkClient.SetDomainCertificate(setDomainCertificateReq)
d.logger.Debug("sdk request 'baishan.SetDomainCertificate'", slog.Any("request", setDomainCertificateReq), slog.Any("response", setDomainCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'baishan.SetDomainCertificate': %w", err)
}
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(apiToken string) (*bssdk.Client, error) {
return bssdk.NewClient(apiToken)
}

View File

@@ -0,0 +1,70 @@
package baishancdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/baishan-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fApiToken string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_BAISHANCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./baishan_cdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_BAISHANCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_BAISHANCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_BAISHANCDN_APITOKEN="your-api-token" \
--CERTIMATE_SSLDEPLOYER_BAISHANCDN_DOMAIN="example.com"
*/
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("APITOKEN: %v", fApiToken),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ApiToken: fApiToken,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,94 @@
package baotapanelconsole
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
btsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/btpanel"
)
type SSLDeployerProviderConfig struct {
// 宝塔面板服务地址。
ServerUrl string `json:"serverUrl"`
// 宝塔面板接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 是否自动重启。
AutoRestart bool `json:"autoRestart"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *btsdk.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 设置面板 SSL 证书
configSavePanelSSLReq := &btsdk.ConfigSavePanelSSLRequest{
PrivateKey: privkeyPEM,
Certificate: certPEM,
}
configSavePanelSSLResp, err := d.sdkClient.ConfigSavePanelSSL(configSavePanelSSLReq)
d.logger.Debug("sdk request 'bt.ConfigSavePanelSSL'", slog.Any("request", configSavePanelSSLReq), slog.Any("response", configSavePanelSSLResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.ConfigSavePanelSSL': %w", err)
}
if d.config.AutoRestart {
// 重启面板(无需关心响应,因为宝塔重启时会断开连接产生 error
systemServiceAdminReq := &btsdk.SystemServiceAdminRequest{
Name: "nginx",
Type: "restart",
}
systemServiceAdminResp, _ := d.sdkClient.SystemServiceAdmin(systemServiceAdminReq)
d.logger.Debug("sdk request 'bt.SystemServiceAdmin'", slog.Any("request", systemServiceAdminReq), slog.Any("response", systemServiceAdminResp))
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) {
client, err := btsdk.NewClient(serverUrl, apiKey)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,72 @@
package baotapanelconsole_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/baotapanel-console"
)
var (
fInputCertPath string
fInputKeyPath string
fServerUrl string
fApiKey string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_BAOTAPANELCONSOLE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./baotapanel_console_test.go -args \
--CERTIMATE_SSLDEPLOYER_BAOTAPANELCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_BAOTAPANELCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_BAOTAPANELCONSOLE_SERVERURL="http://127.0.0.1:8888" \
--CERTIMATE_SSLDEPLOYER_BAOTAPANELCONSOLE_APIKEY="your-api-key"
*/
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("SERVERURL: %v", fServerUrl),
fmt.Sprintf("APIKEY: %v", fApiKey),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ServerUrl: fServerUrl,
ApiKey: fApiKey,
AllowInsecureConnections: true,
AutoRestart: true,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,136 @@
package baotapanelsite
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
btsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/btpanel"
xslices "github.com/usual2970/certimate/internal/pkg/utils/slices"
)
type SSLDeployerProviderConfig struct {
// 宝塔面板服务地址。
ServerUrl string `json:"serverUrl"`
// 宝塔面板接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 网站类型。
SiteType string `json:"siteType"`
// 网站名称(单个)。
SiteName string `json:"siteName,omitempty"`
// 网站名称(多个)。
SiteNames []string `json:"siteNames,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *btsdk.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
switch d.config.SiteType {
case "php":
{
if d.config.SiteName == "" {
return nil, errors.New("config `siteName` is required")
}
// 设置站点 SSL 证书
siteSetSSLReq := &btsdk.SiteSetSSLRequest{
SiteName: d.config.SiteName,
Type: "0",
Certificate: certPEM,
PrivateKey: privkeyPEM,
}
siteSetSSLResp, err := d.sdkClient.SiteSetSSL(siteSetSSLReq)
d.logger.Debug("sdk request 'bt.SiteSetSSL'", slog.Any("request", siteSetSSLReq), slog.Any("response", siteSetSSLResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.SiteSetSSL': %w", err)
}
}
case "other":
{
if len(d.config.SiteNames) == 0 {
return nil, errors.New("config `siteNames` is required")
}
// 上传证书
sslCertSaveCertReq := &btsdk.SSLCertSaveCertRequest{
Certificate: certPEM,
PrivateKey: privkeyPEM,
}
sslCertSaveCertResp, err := d.sdkClient.SSLCertSaveCert(sslCertSaveCertReq)
d.logger.Debug("sdk request 'bt.SSLCertSaveCert'", slog.Any("request", sslCertSaveCertReq), slog.Any("response", sslCertSaveCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.SSLCertSaveCert': %w", err)
}
// 设置站点证书
sslSetBatchCertToSiteReq := &btsdk.SSLSetBatchCertToSiteRequest{
BatchInfo: xslices.Map(d.config.SiteNames, func(siteName string) *btsdk.SSLSetBatchCertToSiteRequestBatchInfo {
return &btsdk.SSLSetBatchCertToSiteRequestBatchInfo{
SiteName: siteName,
SSLHash: sslCertSaveCertResp.SSLHash,
}
}),
}
sslSetBatchCertToSiteResp, err := d.sdkClient.SSLSetBatchCertToSite(sslSetBatchCertToSiteReq)
d.logger.Debug("sdk request 'bt.SSLSetBatchCertToSite'", slog.Any("request", sslSetBatchCertToSiteReq), slog.Any("response", sslSetBatchCertToSiteResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.SSLSetBatchCertToSite': %w", err)
}
}
default:
return nil, fmt.Errorf("unsupported site type '%s'", d.config.SiteType)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) {
client, err := btsdk.NewClient(serverUrl, apiKey)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,82 @@
package baotapanelsite_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/baotapanel-site"
)
var (
fInputCertPath string
fInputKeyPath string
fServerUrl string
fApiKey string
fSiteType string
fSiteName string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_BAOTAPANELSITE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.StringVar(&fSiteType, argsPrefix+"SITETYPE", "", "")
flag.StringVar(&fSiteName, argsPrefix+"SITENAME", "", "")
}
/*
Shell command to run this test:
go test -v ./baotapanel_site_test.go -args \
--CERTIMATE_SSLDEPLOYER_BAOTAPANELSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_BAOTAPANELSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_BAOTAPANELSITE_SERVERURL="http://127.0.0.1:8888" \
--CERTIMATE_SSLDEPLOYER_BAOTAPANELSITE_APIKEY="your-api-key" \
--CERTIMATE_SSLDEPLOYER_BAOTAPANELSITE_SITETYPE="php" \
--CERTIMATE_SSLDEPLOYER_BAOTAPANELSITE_SITENAME="your-site-name"
*/
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("SERVERURL: %v", fServerUrl),
fmt.Sprintf("APIKEY: %v", fApiKey),
fmt.Sprintf("SITETYPE: %v", fSiteType),
fmt.Sprintf("SITENAME: %v", fSiteName),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ServerUrl: fServerUrl,
ApiKey: fApiKey,
AllowInsecureConnections: true,
SiteType: fSiteType,
SiteName: fSiteName,
SiteNames: []string{fSiteName},
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,83 @@
package baotapanelconsole
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
btwafsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/btwaf"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// 堡塔云 WAF 服务地址。
ServerUrl string `json:"serverUrl"`
// 堡塔云 WAF 接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *btwafsdk.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 设置面板 SSL
configSetCertReq := &btwafsdk.ConfigSetCertRequest{
CertContent: xtypes.ToPtr(certPEM),
KeyContent: xtypes.ToPtr(privkeyPEM),
}
configSetCertResp, err := d.sdkClient.ConfigSetCert(configSetCertReq)
d.logger.Debug("sdk request 'bt.ConfigSetCert'", slog.Any("request", configSetCertReq), slog.Any("response", configSetCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.ConfigSetCert': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btwafsdk.Client, error) {
client, err := btwafsdk.NewClient(serverUrl, apiKey)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,73 @@
package baotapanelconsole_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/baotawaf-console"
)
var (
fInputCertPath string
fInputKeyPath string
fServerUrl string
fApiKey string
fSiteName string
fSitePort int64
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_BAOTAWAFCONSOLE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./baotawaf_console_test.go -args \
--CERTIMATE_SSLDEPLOYER_BAOTAWAFCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_BAOTAWAFCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_BAOTAWAFCONSOLE_SERVERURL="http://127.0.0.1:8888" \
--CERTIMATE_SSLDEPLOYER_BAOTAWAFCONSOLE_APIKEY="your-api-key"
*/
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("SERVERURL: %v", fServerUrl),
fmt.Sprintf("APIKEY: %v", fApiKey),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ServerUrl: fServerUrl,
ApiKey: fApiKey,
AllowInsecureConnections: true,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,145 @@
package baotapanelwaf
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
btwafsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/btwaf"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// 堡塔云 WAF 服务地址。
ServerUrl string `json:"serverUrl"`
// 堡塔云 WAF 接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 网站名称。
SiteName string `json:"siteName"`
// 网站 SSL 端口。
// 零值时默认值 443。
SitePort int32 `json:"sitePort,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *btwafsdk.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.SiteName == "" {
return nil, errors.New("config `siteName` is required")
}
if d.config.SitePort == 0 {
d.config.SitePort = 443
}
// 遍历获取网站列表,获取网站 ID
// REF: https://support.huaweicloud.com/api-waf/ListHost.html
siteId := ""
getSitListPage := int32(1)
getSitListPageSize := int32(100)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
getSiteListReq := &btwafsdk.GetSiteListRequest{
SiteName: xtypes.ToPtr(d.config.SiteName),
Page: xtypes.ToPtr(getSitListPage),
PageSize: xtypes.ToPtr(getSitListPageSize),
}
getSiteListResp, err := d.sdkClient.GetSiteList(getSiteListReq)
d.logger.Debug("sdk request 'bt.GetSiteList'", slog.Any("request", getSiteListReq), slog.Any("response", getSiteListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.GetSiteList': %w", err)
}
if getSiteListResp.Result != nil && getSiteListResp.Result.List != nil {
for _, siteItem := range getSiteListResp.Result.List {
if siteItem.SiteName == d.config.SiteName {
siteId = siteItem.SiteId
break
}
}
}
if getSiteListResp.Result == nil || len(getSiteListResp.Result.List) < int(getSitListPageSize) {
break
} else {
getSitListPage++
}
}
if siteId == "" {
return nil, errors.New("site not found")
}
// 修改站点配置
modifySiteReq := &btwafsdk.ModifySiteRequest{
SiteId: xtypes.ToPtr(siteId),
Type: xtypes.ToPtr("openCert"),
Server: &btwafsdk.SiteServerInfo{
ListenSSLPorts: xtypes.ToPtr([]int32{d.config.SitePort}),
SSL: &btwafsdk.SiteServerSSLInfo{
IsSSL: xtypes.ToPtr(int32(1)),
FullChain: xtypes.ToPtr(certPEM),
PrivateKey: xtypes.ToPtr(privkeyPEM),
},
},
}
modifySiteResp, err := d.sdkClient.ModifySite(modifySiteReq)
d.logger.Debug("sdk request 'bt.ModifySite'", slog.Any("request", modifySiteReq), slog.Any("response", modifySiteResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.ModifySite': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btwafsdk.Client, error) {
client, err := btwafsdk.NewClient(serverUrl, apiKey)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,81 @@
package baotapanelwaf_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/baotawaf-site"
)
var (
fInputCertPath string
fInputKeyPath string
fServerUrl string
fApiKey string
fSiteName string
fSitePort int64
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_BAOTAWAFSITE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.StringVar(&fSiteName, argsPrefix+"SITENAME", "", "")
flag.Int64Var(&fSitePort, argsPrefix+"SITEPORT", 0, "")
}
/*
Shell command to run this test:
go test -v ./baotawaf_site_test.go -args \
--CERTIMATE_SSLDEPLOYER_BAOTAWAFSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_BAOTAWAFSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_BAOTAWAFSITE_SERVERURL="http://127.0.0.1:8888" \
--CERTIMATE_SSLDEPLOYER_BAOTAWAFSITE_APIKEY="your-api-key" \
--CERTIMATE_SSLDEPLOYER_BAOTAWAFSITE_SITENAME="your-site-name" \
--CERTIMATE_SSLDEPLOYER_BAOTAWAFSITE_SITEPORT=443
*/
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("SERVERURL: %v", fServerUrl),
fmt.Sprintf("APIKEY: %v", fApiKey),
fmt.Sprintf("SITENAME: %v", fSiteName),
fmt.Sprintf("SITEPORT: %v", fSitePort),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ServerUrl: fServerUrl,
ApiKey: fApiKey,
AllowInsecureConnections: true,
SiteName: fSiteName,
SitePort: int32(fSitePort),
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,81 @@
package bunnycdn
import (
"context"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
bunnysdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/bunny"
)
type SSLDeployerProviderConfig struct {
// Bunny API Key。
ApiKey string `json:"apiKey"`
// Bunny Pull Zone ID。
PullZoneId string `json:"pullZoneId"`
// Bunny CDN Hostname支持泛域名
Hostname string `json:"hostname"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *bunnysdk.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ApiKey)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.PullZoneId == "" {
return nil, fmt.Errorf("config `pullZoneId` is required")
}
if d.config.Hostname == "" {
return nil, fmt.Errorf("config `hostname` is required")
}
// 上传证书
createCertificateReq := &bunnysdk.AddCustomCertificateRequest{
Hostname: d.config.Hostname,
Certificate: base64.StdEncoding.EncodeToString([]byte(certPEM)),
CertificateKey: base64.StdEncoding.EncodeToString([]byte(privkeyPEM)),
}
err := d.sdkClient.AddCustomCertificate(d.config.PullZoneId, createCertificateReq)
d.logger.Debug("sdk request 'bunny.AddCustomCertificate'", slog.Any("request", createCertificateReq))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bunny.AddCustomCertificate': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(apiKey string) (*bunnysdk.Client, error) {
return bunnysdk.NewClient(apiKey)
}

View File

@@ -0,0 +1,75 @@
package bunnycdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/bunny-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fApiKey string
fPullZoneId string
fHostName string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_BUNNYCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.StringVar(&fPullZoneId, argsPrefix+"PULLZONEID", "", "")
flag.StringVar(&fHostName, argsPrefix+"HOSTNAME", "", "")
}
/*
Shell command to run this test:
go test -v ./bunny_cdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_BUNNYCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_BUNNYCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_BUNNYCDN_APITOKEN="your-api-token" \
--CERTIMATE_SSLDEPLOYER_BUNNYCDN_PULLZONEID="your-pull-zone-id" \
--CERTIMATE_SSLDEPLOYER_BUNNYCDN_HOSTNAME="example.com"
*/
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("APIKEY: %v", fApiKey),
fmt.Sprintf("PULLZONEID: %v", fPullZoneId),
fmt.Sprintf("HOSTNAME: %v", fHostName),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ApiKey: fApiKey,
PullZoneId: fPullZoneId,
Hostname: fHostName,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,144 @@
package bytepluscdn
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
bpcdn "github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/byteplus-cdn"
)
type SSLDeployerProviderConfig struct {
// BytePlus AccessKey。
AccessKey string `json:"accessKey"`
// BytePlus SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *bpcdn.CDN
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client := bpcdn.NewInstance()
client.Client.SetAccessKey(config.AccessKey)
client.Client.SetSecretKey(config.SecretKey)
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKey: config.AccessKey,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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)
if strings.HasPrefix(d.config.Domain, "*.") {
// 获取指定证书可关联的域名
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-describecertconfig-9ea17
describeCertConfigReq := &bpcdn.DescribeCertConfigRequest{
CertId: upres.CertId,
}
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
d.logger.Debug("sdk request 'cdn.DescribeCertConfig'", slog.Any("request", describeCertConfigReq), slog.Any("response", describeCertConfigResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeCertConfig': %w", err)
}
if describeCertConfigResp.Result.CertNotConfig != nil {
for i := range describeCertConfigResp.Result.CertNotConfig {
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
}
}
if describeCertConfigResp.Result.OtherCertConfig != nil {
for i := range describeCertConfigResp.Result.OtherCertConfig {
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
}
}
if len(domains) == 0 {
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
// 所有可关联的域名都配置了该证书,跳过部署
d.logger.Info("no domains to deploy")
} else {
return nil, errors.New("domain not found")
}
}
} else {
domains = append(domains, d.config.Domain)
}
if len(domains) > 0 {
var errs []error
for _, domain := range domains {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
// 关联证书与加速域名
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-batchdeploycert
batchDeployCertReq := &bpcdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domain,
}
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
d.logger.Debug("sdk request 'cdn.BatchDeployCert'", slog.Any("request", batchDeployCertReq), slog.Any("response", batchDeployCertResp))
if err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
}
return &core.SSLDeployResult{}, nil
}

View File

@@ -0,0 +1,75 @@
package bytepluscdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/byteplus-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKey string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_BYTEPLUSCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./byteplus_cdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_BYTEPLUSCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_BYTEPLUSCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_BYTEPLUSCDN_ACCESSKEY="your-access-key" \
--CERTIMATE_SSLDEPLOYER_BYTEPLUSCDN_SECRETKEY="your-secret-key" \
--CERTIMATE_SSLDEPLOYER_BYTEPLUSCDN_DOMAIN="example.com"
*/
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("ACCESSKEY: %v", fAccessKey),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKey: fAccessKey,
SecretKey: fSecretKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,70 @@
package cachefly
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
cacheflysdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/cachefly"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// CacheFly API Token。
ApiToken string `json:"apiToken"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *cacheflysdk.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ApiToken)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
// REF: https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post
createCertificateReq := &cacheflysdk.CreateCertificateRequest{
Certificate: xtypes.ToPtr(certPEM),
CertificateKey: xtypes.ToPtr(privkeyPEM),
}
createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
d.logger.Debug("sdk request 'cachefly.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cachefly.CreateCertificate': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(apiToken string) (*cacheflysdk.Client, error) {
return cacheflysdk.NewClient(apiToken)
}

View File

@@ -0,0 +1,65 @@
package cachefly_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/cachefly"
)
var (
fInputCertPath string
fInputKeyPath string
fApiToken string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_CACHEFLY_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
}
/*
Shell command to run this test:
go test -v ./cachefly_test.go -args \
--CERTIMATE_SSLDEPLOYER_CACHEFLY_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_CACHEFLY_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_CACHEFLY_APITOKEN="your-api-token"
*/
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("APITOKEN: %v", fApiToken),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ApiToken: fApiToken,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,166 @@
package cdnfly
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"log/slog"
"time"
"github.com/usual2970/certimate/internal/pkg/core"
cdnflysdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/cdnfly"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// Cdnfly 服务地址。
ServerUrl string `json:"serverUrl"`
// Cdnfly 用户端 API Key。
ApiKey string `json:"apiKey"`
// Cdnfly 用户端 API Secret。
ApiSecret string `json:"apiSecret"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 网站 ID。
// 部署资源类型为 [RESOURCE_TYPE_SITE] 时必填。
SiteId string `json:"siteId,omitempty"`
// 证书 ID。
// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。
CertificateId string `json:"certificateId,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *cdnflysdk.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.ApiSecret, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_SITE:
if err := d.deployToSite(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
case RESOURCE_TYPE_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToSite(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.SiteId == "" {
return errors.New("config `siteId` is required")
}
// 获取单个网站详情
// REF: https://doc.cdnfly.cn/wangzhanguanli-v1-sites.html#%E8%8E%B7%E5%8F%96%E5%8D%95%E4%B8%AA%E7%BD%91%E7%AB%99%E8%AF%A6%E6%83%85
getSiteResp, err := d.sdkClient.GetSite(d.config.SiteId)
d.logger.Debug("sdk request 'cdnfly.GetSite'", slog.Any("siteId", d.config.SiteId), slog.Any("response", getSiteResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'cdnfly.GetSite': %w", err)
}
// 添加单个证书
// REF: https://doc.cdnfly.cn/wangzhanzhengshu-v1-certs.html#%E6%B7%BB%E5%8A%A0%E5%8D%95%E4%B8%AA%E6%88%96%E5%A4%9A%E4%B8%AA%E8%AF%81%E4%B9%A6-%E5%A4%9A%E4%B8%AA%E8%AF%81%E4%B9%A6%E6%97%B6%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F%E4%B8%BA%E6%95%B0%E7%BB%84
createCertificateReq := &cdnflysdk.CreateCertRequest{
Name: xtypes.ToPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
Type: xtypes.ToPtr("custom"),
Cert: xtypes.ToPtr(certPEM),
Key: xtypes.ToPtr(privkeyPEM),
}
createCertificateResp, err := d.sdkClient.CreateCert(createCertificateReq)
d.logger.Debug("sdk request 'cdnfly.CreateCert'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'cdnfly.CreateCert': %w", err)
}
// 修改单个网站
// REF: https://doc.cdnfly.cn/wangzhanguanli-v1-sites.html#%E4%BF%AE%E6%94%B9%E5%8D%95%E4%B8%AA%E7%BD%91%E7%AB%99
updateSiteHttpsListenMap := make(map[string]any)
_ = json.Unmarshal([]byte(getSiteResp.Data.HttpsListen), &updateSiteHttpsListenMap)
updateSiteHttpsListenMap["cert"] = createCertificateResp.Data
updateSiteHttpsListenData, _ := json.Marshal(updateSiteHttpsListenMap)
updateSiteReq := &cdnflysdk.UpdateSiteRequest{
HttpsListen: xtypes.ToPtr(string(updateSiteHttpsListenData)),
}
updateSiteResp, err := d.sdkClient.UpdateSite(d.config.SiteId, updateSiteReq)
d.logger.Debug("sdk request 'cdnfly.UpdateSite'", slog.String("siteId", d.config.SiteId), slog.Any("request", updateSiteReq), slog.Any("response", updateSiteResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'cdnfly.UpdateSite': %w", err)
}
return nil
}
func (d *SSLDeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.CertificateId == "" {
return errors.New("config `certificateId` is required")
}
// 修改单个证书
// REF: https://doc.cdnfly.cn/wangzhanzhengshu-v1-certs.html#%E4%BF%AE%E6%94%B9%E5%8D%95%E4%B8%AA%E8%AF%81%E4%B9%A6
updateCertReq := &cdnflysdk.UpdateCertRequest{
Type: xtypes.ToPtr("custom"),
Cert: xtypes.ToPtr(certPEM),
Key: xtypes.ToPtr(privkeyPEM),
}
updateCertResp, err := d.sdkClient.UpdateCert(d.config.CertificateId, updateCertReq)
d.logger.Debug("sdk request 'cdnfly.UpdateCert'", slog.String("certId", d.config.CertificateId), slog.Any("request", updateCertReq), slog.Any("response", updateCertResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'cdnfly.UpdateCert': %w", err)
}
return nil
}
func createSDKClient(serverUrl, apiKey, apiSecret string, skipTlsVerify bool) (*cdnflysdk.Client, error) {
client, err := cdnflysdk.NewClient(serverUrl, apiKey, apiSecret)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,82 @@
package cdnfly_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/cdnfly"
)
var (
fInputCertPath string
fInputKeyPath string
fServerUrl string
fApiKey string
fApiSecret string
fCertificateId string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_CDNFLY_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.StringVar(&fApiSecret, argsPrefix+"APISECRET", "", "")
flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
}
/*
Shell command to run this test:
go test -v ./cdnfly_test.go -args \
--CERTIMATE_SSLDEPLOYER_CDNFLY_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_CDNFLY_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_CDNFLY_SERVERURL="http://127.0.0.1:88" \
--CERTIMATE_SSLDEPLOYER_CDNFLY_APIKEY="your-api-key" \
--CERTIMATE_SSLDEPLOYER_CDNFLY_APISECRET="your-api-secret" \
--CERTIMATE_SSLDEPLOYER_CDNFLY_CERTIFICATEID="your-cert-id"
*/
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("SERVERURL: %v", fServerUrl),
fmt.Sprintf("APIKEY: %v", fApiKey),
fmt.Sprintf("APISECRET: %v", fApiSecret),
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ServerUrl: fServerUrl,
ApiKey: fApiKey,
ApiSecret: fApiSecret,
AllowInsecureConnections: true,
ResourceType: provider.RESOURCE_TYPE_CERTIFICATE,
CertificateId: fCertificateId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,10 @@
package cdnfly
type ResourceType string
const (
// 资源类型:替换指定网站的证书。
RESOURCE_TYPE_SITE = ResourceType("site")
// 资源类型:替换指定证书。
RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate")
)

View File

@@ -0,0 +1,111 @@
package ctcccloudao
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/ctcccloud-ao"
ctyunao "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/ao"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *ctyunao.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 上传证书
upres, err := d.sslManager.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))
}
// 域名基础及加速配置查询
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13412&data=174&isNormal=1&vid=167
getDomainConfigReq := &ctyunao.GetDomainConfigRequest{
Domain: xtypes.ToPtr(d.config.Domain),
}
getDomainConfigResp, err := d.sdkClient.GetDomainConfig(getDomainConfigReq)
d.logger.Debug("sdk request 'cdn.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.GetDomainConfig': %w", err)
}
// 域名基础及加速配置修改
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13413&data=174&isNormal=1&vid=167
modifyDomainConfigReq := &ctyunao.ModifyDomainConfigRequest{
Domain: xtypes.ToPtr(d.config.Domain),
ProductCode: xtypes.ToPtr(getDomainConfigResp.ReturnObj.ProductCode),
Origin: getDomainConfigResp.ReturnObj.Origin,
HttpsStatus: xtypes.ToPtr("on"),
CertName: xtypes.ToPtr(upres.CertName),
}
modifyDomainConfigResp, err := d.sdkClient.ModifyDomainConfig(modifyDomainConfigReq)
d.logger.Debug("sdk request 'cdn.ModifyDomainConfig'", slog.Any("request", modifyDomainConfigReq), slog.Any("response", modifyDomainConfigResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.ModifyDomainConfig': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunao.Client, error) {
return ctyunao.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,75 @@
package ctcccloudao_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/ctcccloud-ao"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_CTCCCLOUDAO_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_ao_test.go -args \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDAO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDAO_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDAO_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDAO_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDAO_DOMAIN="example.com"
*/
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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,109 @@
package ctcccloudcdn
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/ctcccloud-cdn"
ctyuncdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/cdn"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *ctyuncdn.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 上传证书
upres, err := d.sslManager.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))
}
// 查询域名配置信息
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11304&data=161&isNormal=1&vid=154
queryDomainDetailReq := &ctyuncdn.QueryDomainDetailRequest{
Domain: xtypes.ToPtr(d.config.Domain),
}
queryDomainDetailResp, err := d.sdkClient.QueryDomainDetail(queryDomainDetailReq)
d.logger.Debug("sdk request 'cdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryDomainDetail': %w", err)
}
// 修改域名配置
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11308&data=161&isNormal=1&vid=154
updateDomainReq := &ctyuncdn.UpdateDomainRequest{
Domain: xtypes.ToPtr(d.config.Domain),
HttpsStatus: xtypes.ToPtr("on"),
CertName: xtypes.ToPtr(upres.CertName),
}
updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq)
d.logger.Debug("sdk request 'cdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.UpdateDomain': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, secretAccessKey string) (*ctyuncdn.Client, error) {
return ctyuncdn.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,75 @@
package ctcccloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/ctcccloud-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_cdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_DOMAIN="example.com"
*/
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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,66 @@
package ctcccloudcms
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/ctcccloud-cms"
)
type SSLDeployerProviderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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))
}
return &core.SSLDeployResult{}, nil
}

View File

@@ -0,0 +1,70 @@
package ctcccloudcms_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/ctcccloud-cms"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_CTCCCLOUDCMS_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_cms_test.go -args \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCMS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCMS_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCMS_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCMS_SECRETACCESSKEY="your-secret-access-key"
*/
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("SECRETACCESSKEY: %v", fSecretAccessKey),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,10 @@
package ctcccloudelb
type ResourceType string
const (
// 资源类型:部署到指定负载均衡器。
RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@@ -0,0 +1,197 @@
package ctcccloudelb
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/ctcccloud-elb"
ctyunelb "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/elb"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 天翼云资源池 ID。
RegionId string `json:"regionId"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听器 ID。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *ctyunelb.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
RegionId: config.RegionId,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 上传证书
upres, err := d.sslManager.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))
}
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case RESOURCE_TYPE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
// 查询监听列表
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5654&data=88&isNormal=1&vid=82
listenerIds := make([]string, 0)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenersReq := &ctyunelb.ListListenersRequest{
RegionID: xtypes.ToPtr(d.config.RegionId),
LoadBalancerID: xtypes.ToPtr(d.config.LoadbalancerId),
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
d.logger.Debug("sdk request 'elb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elb.ListListeners': %w", err)
}
for _, listener := range listListenersResp.ReturnObj {
if strings.EqualFold(listener.Protocol, "HTTPS") {
listenerIds = append(listenerIds, listener.ID)
}
}
break
}
// 遍历更新监听证书
if len(listenerIds) == 0 {
d.logger.Info("no elb listeners to deploy")
} else {
d.logger.Info("found https listeners to deploy", slog.Any("listenerIds", listenerIds))
var errs []error
for _, listenerId := range listenerIds {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *SSLDeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 更新监听
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
return err
}
return nil
}
func (d *SSLDeployerProvider) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
// 更新监听器
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5652&data=88&isNormal=1&vid=82
setLoadBalancerHTTPSListenerAttributeReq := &ctyunelb.UpdateListenerRequest{
RegionID: xtypes.ToPtr(d.config.RegionId),
ListenerID: xtypes.ToPtr(cloudListenerId),
CertificateID: xtypes.ToPtr(cloudCertId),
}
setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.UpdateListener(setLoadBalancerHTTPSListenerAttributeReq)
d.logger.Debug("sdk request 'elb.UpdateListener'", slog.Any("request", setLoadBalancerHTTPSListenerAttributeReq), slog.Any("response", setLoadBalancerHTTPSListenerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err)
}
return nil
}
func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunelb.Client, error) {
return ctyunelb.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,118 @@
package ctcccloudelb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/ctcccloud-elb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegionId string
fLoadbalancerId string
fListenerId string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_CTCCCLOUDELB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegionId, argsPrefix+"REGIONID", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_elb_test.go -args \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDELB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDELB_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDELB_REGIONID="your-region-id" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDELB_LOADBALANCERID="your-elb-instance-id" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDELB_LISTENERID="your-elb-listener-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToLoadbalancer", 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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGIONID: %v", fRegionId),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
RegionId: fRegionId,
ResourceType: provider.RESOURCE_TYPE_LOADBALANCER,
LoadbalancerId: fLoadbalancerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListener", 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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGIONID: %v", fRegionId),
fmt.Sprintf("LISTENERID: %v", fListenerId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
RegionId: fRegionId,
ResourceType: provider.RESOURCE_TYPE_LISTENER,
ListenerId: fListenerId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,109 @@
package ctcccloudicdn
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/ctcccloud-icdn"
ctyunicdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/icdn"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *ctyunicdn.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 上传证书
upres, err := d.sslManager.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))
}
// 查询域名配置信息
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10849&data=173&isNormal=1&vid=166
queryDomainDetailReq := &ctyunicdn.QueryDomainDetailRequest{
Domain: xtypes.ToPtr(d.config.Domain),
}
queryDomainDetailResp, err := d.sdkClient.QueryDomainDetail(queryDomainDetailReq)
d.logger.Debug("sdk request 'icdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'icdn.QueryDomainDetail': %w", err)
}
// 修改域名配置
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10853&data=173&isNormal=1&vid=166
updateDomainReq := &ctyunicdn.UpdateDomainRequest{
Domain: xtypes.ToPtr(d.config.Domain),
HttpsStatus: xtypes.ToPtr("on"),
CertName: xtypes.ToPtr(upres.CertName),
}
updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq)
d.logger.Debug("sdk request 'icdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'icdn.UpdateDomain': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunicdn.Client, error) {
return ctyunicdn.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,75 @@
package ctcccloudicdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/ctcccloud-icdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_cdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDCDN_DOMAIN="example.com"
*/
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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,111 @@
package ctcccloudlvdn
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/ctcccloud-lvdn"
ctyunlvdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/lvdn"
xtypes "github.com/usual2970/certimate/internal/pkg/utils/types"
)
type SSLDeployerProviderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 加速域名(不支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *ctyunlvdn.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 上传证书
upres, err := d.sslManager.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))
}
// 查询域名配置信息
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11473&data=183&isNormal=1&vid=261
queryDomainDetailReq := &ctyunlvdn.QueryDomainDetailRequest{
Domain: xtypes.ToPtr(d.config.Domain),
ProductCode: xtypes.ToPtr("005"),
}
queryDomainDetailResp, err := d.sdkClient.QueryDomainDetail(queryDomainDetailReq)
d.logger.Debug("sdk request 'lvdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryDomainDetail': %w", err)
}
// 修改域名配置
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11308&data=161&isNormal=1&vid=154
updateDomainReq := &ctyunlvdn.UpdateDomainRequest{
Domain: xtypes.ToPtr(d.config.Domain),
ProductCode: xtypes.ToPtr("005"),
HttpsSwitch: xtypes.ToPtr(int32(1)),
CertName: xtypes.ToPtr(upres.CertName),
}
updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq)
d.logger.Debug("sdk request 'lvdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'lvdn.UpdateDomain': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunlvdn.Client, error) {
return ctyunlvdn.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,75 @@
package ctcccloudlvdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/ctcccloud-lvdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_CTCCCLOUDLVDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_lvdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDLVDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDLVDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDLVDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDLVDN_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_SSLDEPLOYER_CTCCCLOUDLVDN_DOMAIN="example.com"
*/
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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,100 @@
package dogecloudcdn
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/dogecloud"
dogesdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/dogecloud"
)
type SSLDeployerProviderConfig struct {
// 多吉云 AccessKey。
AccessKey string `json:"accessKey"`
// 多吉云 SecretKey。
SecretKey string `json:"secretKey"`
// 加速域名(不支持泛域名)。
Domain string `json:"domain"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *dogesdk.Client
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.AccessKey, config.SecretKey)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
AccessKey: config.AccessKey,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.Domain == "" {
return nil, fmt.Errorf("config `domain` is required")
}
// 上传证书
upres, err := d.sslManager.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))
}
// 绑定证书
// REF: https://docs.dogecloud.com/cdn/api-cert-bind
bindCdnCertId, _ := strconv.ParseInt(upres.CertId, 10, 64)
bindCdnCertReq := &dogesdk.BindCdnCertRequest{
CertId: bindCdnCertId,
Domain: d.config.Domain,
}
bindCdnCertResp, err := d.sdkClient.BindCdnCert(bindCdnCertReq)
d.logger.Debug("sdk request 'cdn.BindCdnCert'", slog.Any("request", bindCdnCertReq), slog.Any("response", bindCdnCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.BindCdnCert': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(accessKey, secretKey string) (*dogesdk.Client, error) {
return dogesdk.NewClient(accessKey, secretKey)
}

View File

@@ -0,0 +1,75 @@
package dogecloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/dogecloud-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKey string
fSecretKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_DOGECLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./dogecloud_cdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_DOGECLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_DOGECLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_DOGECLOUDCDN_ACCESSKEY="your-access-key" \
--CERTIMATE_SSLDEPLOYER_DOGECLOUDCDN_SECRETKEY="your-secret-key" \
--CERTIMATE_SSLDEPLOYER_DOGECLOUDCDN_DOMAIN="example.com"
*/
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("ACCESSKEY: %v", fAccessKey),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
AccessKey: fAccessKey,
SecretKey: fSecretKey,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,85 @@
package edgioapplications
import (
"context"
"errors"
"fmt"
"log/slog"
edgio "github.com/Edgio/edgio-api/applications/v7"
edgiodtos "github.com/Edgio/edgio-api/applications/v7/dtos"
"github.com/usual2970/certimate/internal/pkg/core"
xcert "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type SSLDeployerProviderConfig struct {
// Edgio ClientId。
ClientId string `json:"clientId"`
// Edgio ClientSecret。
ClientSecret string `json:"clientSecret"`
// Edgio 环境 ID。
EnvironmentId string `json:"environmentId"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *edgio.EdgioClient
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ClientId, config.ClientSecret)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 上传 TLS 证书
// REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts
uploadTlsCertReq := edgiodtos.UploadTlsCertRequest{
EnvironmentID: d.config.EnvironmentId,
PrimaryCert: serverCertPEM,
IntermediateCert: intermediaCertPEM,
PrivateKey: privkeyPEM,
}
uploadTlsCertResp, err := d.sdkClient.UploadTlsCert(uploadTlsCertReq)
d.logger.Debug("sdk request 'edgio.UploadTlsCert'", slog.Any("request", uploadTlsCertReq), slog.Any("response", uploadTlsCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'edgio.UploadTlsCert': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClient(clientId, clientSecret string) (*edgio.EdgioClient, error) {
client := edgio.NewEdgioClient(clientId, clientSecret, "", "")
return client, nil
}

View File

@@ -0,0 +1,75 @@
package edgioapplications_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/edgio-applications"
)
var (
fInputCertPath string
fInputKeyPath string
fClientId string
fClientSecret string
fEnvironmentId string
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_EDGIOAPPLICATIONS_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fClientId, argsPrefix+"CLIENTID", "", "")
flag.StringVar(&fClientSecret, argsPrefix+"CLIENTSECRET", "", "")
flag.StringVar(&fEnvironmentId, argsPrefix+"ENVIRONMENTID", "", "")
}
/*
Shell command to run this test:
go test -v ./edgio_applications_test.go -args \
--CERTIMATE_SSLDEPLOYER_EDGIOAPPLICATIONS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_EDGIOAPPLICATIONS_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_EDGIOAPPLICATIONS_CLIENTID="your-client-id" \
--CERTIMATE_SSLDEPLOYER_EDGIOAPPLICATIONS_CLIENTSECRET="your-client-secret" \
--CERTIMATE_SSLDEPLOYER_EDGIOAPPLICATIONS_ENVIRONMENTID="your-enviroment-id"
*/
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("CLIENTID: %v", fClientId),
fmt.Sprintf("CLIENTSECRET: %v", fClientSecret),
fmt.Sprintf("ENVIRONMENTID: %v", fEnvironmentId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ClientId: fClientId,
ClientSecret: fClientSecret,
EnvironmentId: fEnvironmentId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,8 @@
package flexcdn
type ResourceType string
const (
// 资源类型:替换指定证书。
RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate")
)

View File

@@ -0,0 +1,131 @@
package flexcdn
import (
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"time"
"github.com/usual2970/certimate/internal/pkg/core"
flexcdnsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/flexcdn"
xcert "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type SSLDeployerProviderConfig struct {
// FlexCDN 服务地址。
ServerUrl string `json:"serverUrl"`
// FlexCDN 用户角色。
// 可取值 "user"、"admin"。
ApiRole string `json:"apiRole"`
// FlexCDN AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// FlexCDN AccessKey。
AccessKey string `json:"accessKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 证书 ID。
// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。
CertificateId int64 `json:"certificateId,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *flexcdnsdk.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.CertificateId == 0 {
return errors.New("config `certificateId` is required")
}
// 解析证书内容
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return err
}
// 修改证书
// REF: https://flexcdn.cloud/dev/api/service/SSLCertService?role=user#updateSSLCert
updateSSLCertReq := &flexcdnsdk.UpdateSSLCertRequest{
SSLCertId: d.config.CertificateId,
IsOn: true,
Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
Description: "upload from certimate",
ServerName: certX509.Subject.CommonName,
IsCA: false,
CertData: base64.StdEncoding.EncodeToString([]byte(certPEM)),
KeyData: base64.StdEncoding.EncodeToString([]byte(privkeyPEM)),
TimeBeginAt: certX509.NotBefore.Unix(),
TimeEndAt: certX509.NotAfter.Unix(),
DNSNames: certX509.DNSNames,
CommonNames: []string{certX509.Subject.CommonName},
}
updateSSLCertResp, err := d.sdkClient.UpdateSSLCert(updateSSLCertReq)
d.logger.Debug("sdk request 'flexcdn.UpdateSSLCert'", slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'flexcdn.UpdateSSLCert': %w", err)
}
return nil
}
func createSDKClient(serverUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*flexcdnsdk.Client, error) {
client, err := flexcdnsdk.NewClient(serverUrl, apiRole, accessKeyId, accessKey)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,83 @@
package flexcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/flexcdn"
)
var (
fInputCertPath string
fInputKeyPath string
fServerUrl string
fAccessKeyId string
fAccessKey string
fCertificateId int64
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_FLEXCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "")
}
/*
Shell command to run this test:
go test -v ./flexcdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_FLEXCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_FLEXCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_FLEXCDN_SERVERURL="http://127.0.0.1:7788" \
--CERTIMATE_SSLDEPLOYER_FLEXCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_SSLDEPLOYER_FLEXCDN_ACCESSKEY="your-access-key" \
--CERTIMATE_SSLDEPLOYER_FLEXCDN_CERTIFICATEID="your-cerficiate-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToCertificate", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("SERVERURL: %v", fServerUrl),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ServerUrl: fServerUrl,
ApiRole: "user",
AccessKeyId: fAccessKeyId,
AccessKey: fAccessKey,
AllowInsecureConnections: true,
ResourceType: provider.RESOURCE_TYPE_CERTIFICATE,
CertificateId: fCertificateId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,173 @@
package gcorecdn
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"github.com/G-Core/gcorelabscdn-go/gcore"
"github.com/G-Core/gcorelabscdn-go/gcore/provider"
"github.com/G-Core/gcorelabscdn-go/resources"
"github.com/G-Core/gcorelabscdn-go/sslcerts"
"github.com/usual2970/certimate/internal/pkg/core"
sslmgrsp "github.com/usual2970/certimate/internal/pkg/core/ssl-manager/providers/gcore-cdn"
gcoresdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/gcore"
)
type SSLDeployerProviderConfig struct {
// Gcore API Token。
ApiToken string `json:"apiToken"`
// CDN 资源 ID。
ResourceId int64 `json:"resourceId"`
// 证书 ID。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateId int64 `json:"certificateId,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClients *wSDKClients
sslManager core.SSLManager
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
type wSDKClients struct {
Resources *resources.Service
SSLCerts *sslcerts.Service
}
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
clients, err := createSDKClients(config.ApiToken)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
ApiToken: config.ApiToken,
})
if err != nil {
return nil, fmt.Errorf("could not create ssl manager: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClients: clients,
sslManager: sslmgr,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslManager.SetLogger(logger)
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
if d.config.ResourceId == 0 {
return nil, errors.New("config `resourceId` is required")
}
// 如果原证书 ID 为空,则创建证书;否则更新证书。
var cloudCertId int64
if d.config.CertificateId == 0 {
// 上传证书
upres, err := d.sslManager.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))
}
cloudCertId, _ = strconv.ParseInt(upres.CertId, 10, 64)
} else {
// 获取证书
// REF: https://api.gcore.com/docs/cdn#tag/SSL-certificates/paths/~1cdn~1sslData~1%7Bssl_id%7D/get
getCertificateDetailResp, err := d.sdkClients.SSLCerts.Get(context.TODO(), d.config.CertificateId)
d.logger.Debug("sdk request 'sslcerts.Get'", slog.Any("sslId", d.config.CertificateId), slog.Any("response", getCertificateDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'sslcerts.Get': %w", err)
}
// 更新证书
// REF: https://api.gcore.com/docs/cdn#tag/SSL-certificates/paths/~1cdn~1sslData~1%7Bssl_id%7D/get
changeCertificateReq := &sslcerts.UpdateRequest{
Name: getCertificateDetailResp.Name,
Cert: certPEM,
PrivateKey: privkeyPEM,
ValidateRootCA: false,
}
changeCertificateResp, err := d.sdkClients.SSLCerts.Update(context.TODO(), getCertificateDetailResp.ID, changeCertificateReq)
d.logger.Debug("sdk request 'sslcerts.Update'", slog.Any("sslId", getCertificateDetailResp.ID), slog.Any("request", changeCertificateReq), slog.Any("response", changeCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'sslcerts.Update': %w", err)
}
cloudCertId = changeCertificateResp.ID
}
// 获取 CDN 资源详情
// REF: https://api.gcore.com/docs/cdn#tag/CDN-resources/paths/~1cdn~1resources~1%7Bresource_id%7D/get
getResourceResp, err := d.sdkClients.Resources.Get(context.TODO(), d.config.ResourceId)
d.logger.Debug("sdk request 'resources.Get'", slog.Any("resourceId", d.config.ResourceId), slog.Any("response", getResourceResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'resources.Get': %w", err)
}
// 更新 CDN 资源详情
// REF: https://api.gcore.com/docs/cdn#tag/CDN-resources/operation/change_cdn_resource
updateResourceReq := &resources.UpdateRequest{
Description: getResourceResp.Description,
Active: getResourceResp.Active,
OriginGroup: int(getResourceResp.OriginGroup),
OriginProtocol: getResourceResp.OriginProtocol,
SecondaryHostnames: getResourceResp.SecondaryHostnames,
SSlEnabled: true,
SSLData: int(cloudCertId),
ProxySSLEnabled: getResourceResp.ProxySSLEnabled,
Options: &gcore.Options{},
}
if getResourceResp.ProxySSLCA != 0 {
updateResourceReq.ProxySSLCA = &getResourceResp.ProxySSLCA
}
if getResourceResp.ProxySSLData != 0 {
updateResourceReq.ProxySSLData = &getResourceResp.ProxySSLData
}
updateResourceResp, err := d.sdkClients.Resources.Update(context.TODO(), d.config.ResourceId, updateResourceReq)
d.logger.Debug("sdk request 'resources.Update'", slog.Int64("resourceId", d.config.ResourceId), slog.Any("request", updateResourceReq), slog.Any("response", updateResourceResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'resources.Update': %w", err)
}
return &core.SSLDeployResult{}, nil
}
func createSDKClients(apiToken string) (*wSDKClients, error) {
if apiToken == "" {
return nil, errors.New("invalid gcore api token")
}
requester := provider.NewClient(
gcoresdk.BASE_URL,
provider.WithSigner(gcoresdk.NewAuthRequestSigner(apiToken)),
)
resourcesSrv := resources.NewService(requester)
sslCertsSrv := sslcerts.NewService(requester)
return &wSDKClients{
Resources: resourcesSrv,
SSLCerts: sslCertsSrv,
}, nil
}

View File

@@ -0,0 +1,70 @@
package gcorecdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/ssl-deployer/providers/gcore-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fApiToken string
fResourceId int64
)
func init() {
argsPrefix := "CERTIMATE_SSLDEPLOYER_GCORECDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
flag.Int64Var(&fResourceId, argsPrefix+"RESOURCEID", 0, "")
}
/*
Shell command to run this test:
go test -v ./gcore_cdn_test.go -args \
--CERTIMATE_SSLDEPLOYER_GCORECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_SSLDEPLOYER_GCORECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_SSLDEPLOYER_GCORECDN_APITOKEN="your-api-token" \
--CERTIMATE_SSLDEPLOYER_GCORECDN_RESOURCEID="your-cdn-resource-id"
*/
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("APITOKEN: %v", fApiToken),
fmt.Sprintf("RESOURCEID: %v", fResourceId),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
ApiToken: fApiToken,
ResourceId: fResourceId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,8 @@
package goedge
type ResourceType string
const (
// 资源类型:替换指定证书。
RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate")
)

View File

@@ -0,0 +1,131 @@
package goedge
import (
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"time"
"github.com/usual2970/certimate/internal/pkg/core"
goedgesdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/goedge"
xcert "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type SSLDeployerProviderConfig struct {
// GoEdge 服务地址。
ServerUrl string `json:"serverUrl"`
// GoEdge 用户角色。
// 可取值 "user"、"admin"。
ApiRole string `json:"apiRole"`
// GoEdge AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// GoEdge AccessKey。
AccessKey string `json:"accessKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 证书 ID。
// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。
CertificateId int64 `json:"certificateId,omitempty"`
}
type SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
sdkClient *goedgesdk.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create sdk client: %w", err)
}
return &SSLDeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &core.SSLDeployResult{}, nil
}
func (d *SSLDeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.CertificateId == 0 {
return errors.New("config `certificateId` is required")
}
// 解析证书内容
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return err
}
// 修改证书
// REF: https://goedge.cloud/dev/api/service/SSLCertService?role=user#updateSSLCert
updateSSLCertReq := &goedgesdk.UpdateSSLCertRequest{
SSLCertId: d.config.CertificateId,
IsOn: true,
Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
Description: "upload from certimate",
ServerName: certX509.Subject.CommonName,
IsCA: false,
CertData: base64.StdEncoding.EncodeToString([]byte(certPEM)),
KeyData: base64.StdEncoding.EncodeToString([]byte(privkeyPEM)),
TimeBeginAt: certX509.NotBefore.Unix(),
TimeEndAt: certX509.NotAfter.Unix(),
DNSNames: certX509.DNSNames,
CommonNames: []string{certX509.Subject.CommonName},
}
updateSSLCertResp, err := d.sdkClient.UpdateSSLCert(updateSSLCertReq)
d.logger.Debug("sdk request 'goedge.UpdateSSLCert'", slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'goedge.UpdateSSLCert': %w", err)
}
return nil
}
func createSDKClient(serverUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*goedgesdk.Client, error) {
client, err := goedgesdk.NewClient(serverUrl, apiRole, accessKeyId, accessKey)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

Some files were not shown because too many files have changed in this diff Show More