diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 92b42c78..1903d2eb 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -58,6 +58,7 @@ import ( pGoEdge "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/goedge" pHuaweiCloudCDN "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/huaweicloud-cdn" pHuaweiCloudELB "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/huaweicloud-elb" + pHuaweiCloudOBS "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/huaweicloud-obs" pHuaweiCloudSCM "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/huaweicloud-scm" pHuaweiCloudWAF "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/huaweicloud-waf" pJDCloudALB "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/jdcloud-alb" @@ -784,7 +785,7 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy return deployer, err } - case domain.DeploymentProviderTypeHuaweiCloudCDN, domain.DeploymentProviderTypeHuaweiCloudELB, domain.DeploymentProviderTypeHuaweiCloudSCM, domain.DeploymentProviderTypeHuaweiCloudWAF: + case domain.DeploymentProviderTypeHuaweiCloudCDN, domain.DeploymentProviderTypeHuaweiCloudELB, domain.DeploymentProviderTypeHuaweiCloudSCM, domain.DeploymentProviderTypeHuaweiCloudOBS, domain.DeploymentProviderTypeHuaweiCloudWAF: { access := domain.AccessConfigForHuaweiCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -823,6 +824,16 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy }) return deployer, err + case domain.DeploymentProviderTypeHuaweiCloudOBS: + deployer, err := pHuaweiCloudOBS.NewSSLDeployerProvider(&pHuaweiCloudOBS.SSLDeployerProviderConfig{ + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + Endpoint: xmaps.GetString(options.ProviderServiceConfig, "endpoint"), + Bucket: xmaps.GetString(options.ProviderServiceConfig, "bucket"), + Domain: xmaps.GetString(options.ProviderServiceConfig, "domain"), + }) + return deployer, err + case domain.DeploymentProviderTypeHuaweiCloudWAF: deployer, err := pHuaweiCloudWAF.NewSSLDeployerProvider(&pHuaweiCloudWAF.SSLDeployerProviderConfig{ AccessKeyId: access.AccessKeyId, diff --git a/internal/domain/provider.go b/internal/domain/provider.go index c6e7b969..d6fc293c 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -232,6 +232,7 @@ const ( DeploymentProviderTypeHuaweiCloudCDN = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-cdn") DeploymentProviderTypeHuaweiCloudELB = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-elb") DeploymentProviderTypeHuaweiCloudSCM = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-scm") + DeploymentProviderTypeHuaweiCloudOBS = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-obs") DeploymentProviderTypeHuaweiCloudWAF = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-waf") DeploymentProviderTypeJDCloudALB = DeploymentProviderType(AccessProviderTypeJDCloud + "-alb") DeploymentProviderTypeJDCloudCDN = DeploymentProviderType(AccessProviderTypeJDCloud + "-cdn") diff --git a/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs.go b/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs.go new file mode 100644 index 00000000..6e8c2fa5 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs.go @@ -0,0 +1,124 @@ +package huaweicloudobs + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/md5" + "crypto/sha1" + "encoding/base64" + "errors" + "fmt" + "log/slog" + "net/http" + "strings" + "time" + + "github.com/certimate-go/certimate/pkg/core" +) + +type SSLDeployerProviderConfig struct { + // 华为云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 华为云 SecretAccessKey。 + SecretAccessKey string `json:"secretAccessKey"` + // 华为云 Bucket 对应的 Endpoint。 + Endpoint string `json:"endpoint"` + // 华为云 OBS 桶名。 + Bucket string `json:"bucket"` + // 自定义域名。 + Domain string `json:"domain"` +} + +type SSLDeployerProvider struct { + config *SSLDeployerProviderConfig + logger *slog.Logger +} + +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") + } + + config.Endpoint = strings.TrimPrefix(strings.TrimPrefix(config.Endpoint, "http://"), "https://") + + return &SSLDeployerProvider{ + config: config, + logger: slog.Default(), + }, nil +} + +func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } +} + +// REF: https://support.huaweicloud.com/usermanual-obs/obs_06_3200.html +// REF: https://support.huaweicloud.com/api-obs/obs_04_0059.html +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") + } + + url := fmt.Sprintf("https://%s.%s/?customdomain=%s", d.config.Bucket, d.config.Endpoint, d.config.Domain) + bodyXML := fmt.Sprintf(` + + %s + %s + %s + %s +`, + d.config.Bucket+"_"+d.config.Domain, certPEM, certPEM, privkeyPEM, + ) + + // 计算 Content-MD5(Base64 编码) + md5sum := md5.Sum([]byte(bodyXML)) + contentMD5 := base64.StdEncoding.EncodeToString(md5sum[:]) + + // 日期 + date := time.Now().UTC().Format(http.TimeFormat) + + // 构造签名字符串 + method := "PUT" + contentType := "application/xml" + canonicalizedResource := fmt.Sprintf("/%s/?customdomain=%s", d.config.Bucket, d.config.Domain) + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s", method, contentMD5, contentType, date, canonicalizedResource) + + // HMAC-SHA1 签名 + h := hmac.New(sha1.New, []byte(d.config.SecretAccessKey)) + h.Write([]byte(stringToSign)) + signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) + + // Authorization + authHeader := fmt.Sprintf("OBS %s:%s", d.config.AccessKeyId, signature) + + // 创建请求 + req, err := http.NewRequest(method, url, bytes.NewBuffer([]byte(bodyXML))) + if err != nil { + return nil, err + } + + req.Header.Set("Date", date) + req.Header.Set("Authorization", authHeader) + req.Header.Set("Content-MD5", contentMD5) + req.Header.Set("Content-Type", contentType) + + // 请求 + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body := new(bytes.Buffer) + body.ReadFrom(resp.Body) + return nil, fmt.Errorf("HTTP request failed with status %d: %s", resp.StatusCode, body.String()) + } + return &core.SSLDeployResult{}, nil +} diff --git a/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs_test.go b/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs_test.go new file mode 100644 index 00000000..a1d06f00 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs_test.go @@ -0,0 +1,85 @@ +package huaweicloudobs_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/huaweicloud-obs" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fSecretAccessKey string + fEndpoint string + fBucket string + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") + flag.StringVar(&fEndpoint, argsPrefix+"ENDPOINT", "", "") + flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") +} + +/* +Shell command to run this test: + + go test -v ./huaweicloud_obs_test.go -args \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_ENDPOINT="https://your-endpoint" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_BUCKET="your-bucket" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_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("ENDPOINT: %v", fEndpoint), + fmt.Sprintf("BUCKET: %v", fBucket), + fmt.Sprintf("DOMAIN: %v", fDomain), + }, "\n")) + + deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{ + AccessKeyId: fAccessKeyId, + SecretAccessKey: fSecretAccessKey, + Endpoint: fEndpoint, + 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) + }) +}