From 16f20dc01df9b6481bcf3d569343db99ec253079 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 20 Mar 2025 21:43:09 +0800 Subject: [PATCH] feat: add upyun ssl uploader --- .../providers/qiniu-sslcert/qiniu_sslcert.go | 11 +- .../uploader/providers/upyun-ssl/upyun_ssl.go | 83 +++++++++++ .../providers/upyun-ssl/upyun_ssl_test.go | 72 +++++++++ internal/pkg/vendors/upyun-sdk/console/api.go | 104 +++++++++++++ .../pkg/vendors/upyun-sdk/console/client.go | 94 ++++++++++++ .../pkg/vendors/upyun-sdk/console/models.go | 141 ++++++++++++++++++ 6 files changed, 504 insertions(+), 1 deletion(-) create mode 100644 internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go create mode 100644 internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl_test.go create mode 100644 internal/pkg/vendors/upyun-sdk/console/api.go create mode 100644 internal/pkg/vendors/upyun-sdk/console/client.go create mode 100644 internal/pkg/vendors/upyun-sdk/console/models.go diff --git a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go index ce18a335..6bc71c3f 100644 --- a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go +++ b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go @@ -2,6 +2,7 @@ import ( "context" + "errors" "fmt" "log/slog" "time" @@ -69,7 +70,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe // 上传新证书 // REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate uploadSslCertResp, err := u.sdkClient.UploadSslCert(context.TODO(), certName, certX509.Subject.CommonName, certPem, privkeyPem) - u.logger.Debug("sdk request 'ssl.UploadCertificate'", slog.Any("response", uploadSslCertResp)) + u.logger.Debug("sdk request 'cdn.UploadSslCert'", slog.Any("response", uploadSslCertResp)) if err != nil { return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadSslCert'") } @@ -82,6 +83,14 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe } func createSdkClient(accessKey, secretKey string) (*qiniusdk.Client, error) { + if secretKey == "" { + return nil, errors.New("invalid qiniu access key") + } + + if secretKey == "" { + return nil, errors.New("invalid qiniu secret key") + } + credential := auth.New(accessKey, secretKey) client := qiniusdk.NewClient(credential) return client, nil diff --git a/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go b/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go new file mode 100644 index 00000000..3e4d3c40 --- /dev/null +++ b/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go @@ -0,0 +1,83 @@ +package upyunssl + +import ( + "context" + "errors" + "log/slog" + + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/uploader" + upyunsdk "github.com/usual2970/certimate/internal/pkg/vendors/upyun-sdk/console" +) + +type UploaderConfig struct { + // 又拍云账号用户名。 + Username string `json:"username"` + // 又拍云账号密码。 + Password string `json:"password"` +} + +type UploaderProvider struct { + config *UploaderConfig + logger *slog.Logger + sdkClient *upyunsdk.Client +} + +var _ uploader.Uploader = (*UploaderProvider)(nil) + +func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.Username, config.Password) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + return &UploaderProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { + if logger == nil { + u.logger = slog.Default() + } else { + u.logger = logger + } + return u +} + +func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { + // 上传证书 + uploadHttpsCertificateReq := &upyunsdk.UploadHttpsCertificateRequest{ + Certificate: certPem, + PrivateKey: privkeyPem, + } + uploadHttpsCertificateResp, err := u.sdkClient.UploadHttpsCertificate(uploadHttpsCertificateReq) + u.logger.Debug("sdk request 'ssl.UploadHttpsCertificate'", slog.Any("response", uploadHttpsCertificateResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.UploadHttpsCertificate'") + } + + return &uploader.UploadResult{ + CertId: uploadHttpsCertificateResp.Data.Result.CertificateId, + }, nil +} + +func createSdkClient(username, password string) (*upyunsdk.Client, error) { + if username == "" { + return nil, errors.New("invalid upyun username") + } + + if password == "" { + return nil, errors.New("invalid upyun password") + } + + client := upyunsdk.NewClient(username, password) + return client, nil +} diff --git a/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl_test.go b/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl_test.go new file mode 100644 index 00000000..1e6d81ec --- /dev/null +++ b/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl_test.go @@ -0,0 +1,72 @@ +package upyunssl_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/upyun-ssl" +) + +var ( + fInputCertPath string + fInputKeyPath string + fUsername string + fPassword string +) + +func init() { + argsPrefix := "CERTIMATE_UPLOADER_UPYUNSSL_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") + flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") +} + +/* +Shell command to run this test: + + go test -v ./upyun_ssl_test.go -args \ + --CERTIMATE_UPLOADER_UPYUNSSL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_UPLOADER_UPYUNSSL_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_UPLOADER_UPYUNSSL_USERNAME="your-username" \ + --CERTIMATE_UPLOADER_UPYUNSSL_PASSWORD="your-password" +*/ +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("USERNAME: %v", fUsername), + fmt.Sprintf("PASSWORD: %v", fPassword), + }, "\n")) + + uploader, err := provider.NewUploader(&provider.UploaderConfig{ + Username: fUsername, + Password: fPassword, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + sres, _ := json.Marshal(res) + t.Logf("ok: %s", string(sres)) + }) +} diff --git a/internal/pkg/vendors/upyun-sdk/console/api.go b/internal/pkg/vendors/upyun-sdk/console/api.go new file mode 100644 index 00000000..afcb0b5b --- /dev/null +++ b/internal/pkg/vendors/upyun-sdk/console/api.go @@ -0,0 +1,104 @@ +package console + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" +) + +func (c *Client) getCookie() error { + req := &signinRequest{Username: c.username, Password: c.password} + res, err := c.sendRequest(http.MethodPost, "/accounts/signin/", req) + if err != nil { + return err + } + + resp := &signinResponse{} + if err := json.Unmarshal(res.Body(), &resp); err != nil { + return fmt.Errorf("upyun api error: failed to parse response: %w", err) + } else if !resp.Data.Result { + return errors.New("upyun console signin failed") + } + + c.loginCookie = res.Header().Get("Set-Cookie") + + return nil +} + +func (c *Client) UploadHttpsCertificate(req *UploadHttpsCertificateRequest) (*UploadHttpsCertificateResponse, error) { + if c.loginCookie == "" { + if err := c.getCookie(); err != nil { + return nil, err + } + } + + resp := UploadHttpsCertificateResponse{} + err := c.sendRequestWithResult(http.MethodPost, "/api/https/certificate/", req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (c *Client) GetHttpsCertificateManager(certificateId string) (*GetHttpsCertificateManagerResponse, error) { + if c.loginCookie == "" { + if err := c.getCookie(); err != nil { + return nil, err + } + } + + req := GetHttpsCertificateManagerRequest{CertificateId: certificateId} + resp := GetHttpsCertificateManagerResponse{} + err := c.sendRequestWithResult(http.MethodGet, "/api/https/certificate/manager/", &req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (c *Client) UpdateHttpsCertificateManager(req *UpdateHttpsCertificateManagerRequest) (*UpdateHttpsCertificateManagerResponse, error) { + if c.loginCookie == "" { + if err := c.getCookie(); err != nil { + return nil, err + } + } + + resp := UpdateHttpsCertificateManagerResponse{} + err := c.sendRequestWithResult(http.MethodPost, "/api/https/certificate/manager", req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (c *Client) GetHttpsServiceManager(domain string) (*GetHttpsServiceManagerResponse, error) { + if c.loginCookie == "" { + if err := c.getCookie(); err != nil { + return nil, err + } + } + + req := GetHttpsServiceManagerRequest{Domain: domain} + resp := GetHttpsServiceManagerResponse{} + err := c.sendRequestWithResult(http.MethodGet, "/api/https/services/manager", &req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (c *Client) MigrateHttpsDomain(req *MigrateHttpsDomainRequest) (*MigrateHttpsDomainResponse, error) { + if c.loginCookie == "" { + if err := c.getCookie(); err != nil { + return nil, err + } + } + + resp := MigrateHttpsDomainResponse{} + err := c.sendRequestWithResult(http.MethodPost, "/api/https/migrate/domain", req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} diff --git a/internal/pkg/vendors/upyun-sdk/console/client.go b/internal/pkg/vendors/upyun-sdk/console/client.go new file mode 100644 index 00000000..74758b82 --- /dev/null +++ b/internal/pkg/vendors/upyun-sdk/console/client.go @@ -0,0 +1,94 @@ +package console + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + username string + password string + loginCookie string + + client *resty.Client +} + +func NewClient(username, password string) *Client { + client := resty.New() + + return &Client{ + username: username, + password: password, + client: client, + } +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) { + req := c.client.R().SetBasicAuth(c.username, c.password) + req.Method = method + req.URL = "https://console.upyun.com" + path + if strings.EqualFold(method, http.MethodGet) { + qs := make(map[string]string) + if params != nil { + temp := make(map[string]any) + jsonb, _ := json.Marshal(params) + json.Unmarshal(jsonb, &temp) + for k, v := range temp { + if v != nil { + qs[k] = fmt.Sprintf("%v", v) + } + } + } + + req = req. + SetQueryParams(qs). + SetHeader("Cookie", c.loginCookie) + } else { + req = req. + SetHeader("Content-Type", "application/json"). + SetHeader("Cookie", c.loginCookie). + SetBody(params) + } + + req = req.SetDebug(true) + resp, err := req.Send() + if err != nil { + return nil, fmt.Errorf("upyun api error: failed to send request: %w", err) + } else if resp.IsError() { + return nil, fmt.Errorf("upyun api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + } + + return resp, nil +} + +func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result interface{}) error { + resp, err := c.sendRequest(method, path, params) + if err != nil { + return err + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return fmt.Errorf("upyun api error: failed to parse response: %w", err) + } + + tresp := &baseResponse{} + if err := json.Unmarshal(resp.Body(), &tresp); err != nil { + return fmt.Errorf("upyun api error: failed to parse response: %w", err) + } else if tdata := tresp.GetData(); tdata == nil { + return fmt.Errorf("upyun api error: empty data") + } else if errcode := tdata.GetErrorCode(); errcode > 0 { + return fmt.Errorf("upyun api error: %d - %s", errcode, tdata.GetErrorMessage()) + } + + return nil +} diff --git a/internal/pkg/vendors/upyun-sdk/console/models.go b/internal/pkg/vendors/upyun-sdk/console/models.go new file mode 100644 index 00000000..982993fe --- /dev/null +++ b/internal/pkg/vendors/upyun-sdk/console/models.go @@ -0,0 +1,141 @@ +package console + +import ( + "encoding/json" +) + +type baseResponse struct { + Data *baseResponseData `json:"data,omitempty"` +} + +func (r *baseResponse) GetData() *baseResponseData { + return r.Data +} + +type baseResponseData struct { + ErrorCode json.Number `json:"error_code"` + ErrorMessage string `json:"message"` +} + +func (r *baseResponseData) GetErrorCode() int { + if r.ErrorCode.String() == "" { + return 0 + } + + errcode, err := r.ErrorCode.Int64() + if err != nil { + return -1 + } + + return int(errcode) +} + +func (r *baseResponseData) GetErrorMessage() string { + return r.ErrorMessage +} + +type signinRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type signinResponse struct { + baseResponse + Data struct { + baseResponseData + Result bool `json:"result"` + } `json:"data"` +} + +type UploadHttpsCertificateRequest struct { + Certificate string `json:"certificate"` + PrivateKey string `json:"private_key"` +} + +type UploadHttpsCertificateResponse struct { + baseResponse + Data *struct { + baseResponseData + Status int `json:"status"` + Result struct { + CertificateId string `json:"certificate_id"` + CommonName string `json:"commonName"` + Serial string `json:"serial"` + } `json:"result"` + } `json:"data"` +} + +type GetHttpsCertificateManagerRequest struct { + CertificateId string `json:"certificate_id"` +} + +type GetHttpsCertificateManagerResponse struct { + baseResponse + Data *struct { + baseResponseData + AuthenticateNum int32 `json:"authenticate_num"` + AuthenticateDomains []string `json:"authenticate_domain"` + Domains []HttpsCertificateManagerDomain `json:"domains"` + } `json:"data"` +} + +type HttpsCertificateManagerDomain struct { + Name string `json:"name"` + Type string `json:"type"` + BucketId int64 `json:"bucket_id"` + BucketName string `json:"bucket_name"` +} + +type UpdateHttpsCertificateManagerRequest struct { + CertificateId string `json:"certificate_id"` + Domain string `json:"domain"` + Https bool `json:"https"` + ForceHttps bool `json:"force_https"` +} + +type UpdateHttpsCertificateManagerResponse struct { + baseResponse + Data *struct { + baseResponseData + Status bool `json:"status"` + } `json:"data"` +} + +type GetHttpsServiceManagerRequest struct { + Domain string `json:"domain"` +} + +type GetHttpsServiceManagerResponse struct { + baseResponse + Data *struct { + baseResponseData + Status int `json:"status"` + Domains []HttpsServiceManagerDomain `json:"result"` + } `json:"data"` +} + +type HttpsServiceManagerDomain struct { + CertificateId string `json:"certificate_id"` + CommonName string `json:"commonName"` + Https bool `json:"https"` + ForceHttps bool `json:"force_https"` + PaymentType string `json:"payment_type"` + DomainType string `json:"domain_type"` + Validity struct { + Start int64 `json:"start"` + End int64 `json:"end"` + } `json:"validity"` +} + +type MigrateHttpsDomainRequest struct { + CertificateId string `json:"crt_id"` + Domain string `json:"domain_name"` +} + +type MigrateHttpsDomainResponse struct { + baseResponse + Data *struct { + baseResponseData + Status bool `json:"status"` + } `json:"data"` +}