diff --git a/go.mod b/go.mod index 493943ebb..b0f71d564 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,6 @@ require ( github.com/alibabacloud-go/tea-utils/v2 v2.0.9 github.com/alibabacloud-go/vod-20170321/v4 v4.11.3 github.com/alibabacloud-go/waf-openapi-20211001/v7 v7.8.1 - github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.5.1 github.com/aws/aws-sdk-go-v2 v1.41.12 github.com/aws/aws-sdk-go-v2/config v1.32.23 github.com/aws/aws-sdk-go-v2/credentials v1.19.22 diff --git a/go.sum b/go.sum index 8ec0529ce..87e5918b9 100644 --- a/go.sum +++ b/go.sum @@ -176,8 +176,6 @@ github.com/alibabacloud-go/vod-20170321/v4 v4.11.3 h1:xF9GzlR4Qr+H2MRu558rpJUG6s github.com/alibabacloud-go/vod-20170321/v4 v4.11.3/go.mod h1:2NX/9lVaKpd1+1GEV5zUAzQFfK9pF8Wkx81ugAnHYiw= github.com/alibabacloud-go/waf-openapi-20211001/v7 v7.8.1 h1:zCZIxEfoC3SmPx6x+7JzxXBPkQZ7eA19YFtWWJ80HMs= github.com/alibabacloud-go/waf-openapi-20211001/v7 v7.8.1/go.mod h1:oP48y3ec8gOIGFXcxKEUMiQwO9mZmlAH4TFZZvInHik= -github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.5.1 h1:vtiFd0hhPAbyYJjztl0wYUq/PqEGkIlDmVuTIy6zw8Y= -github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.5.1/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M= github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= diff --git a/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go b/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go index 5bcf64980..f2571075e 100644 --- a/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go +++ b/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go @@ -4,12 +4,14 @@ import ( "context" "fmt" "log/slog" + "strings" "github.com/alibabacloud-go/tea/tea" - "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss" - "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials" + "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core" + cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" + osssdk "github.com/certimate-go/certimate/pkg/sdk3rd/alibabacloud/oss" ) type ( @@ -33,9 +35,10 @@ type DeployerConfig struct { } type Deployer struct { - config *DeployerConfig - logger *slog.Logger - sdkClient *oss.Client + config *DeployerConfig + logger *slog.Logger + sdkClient *osssdk.Client + sdkCertmgr core.Certmgr } var _ Provider = (*Deployer)(nil) @@ -45,15 +48,28 @@ func NewDeployer(config *DeployerConfig) (*Deployer, error) { return nil, fmt.Errorf("the configuration of the deployer provider is nil") } - client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) + client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region, config.Bucket) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } + pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{ + AccessKeyId: config.AccessKeyId, + AccessKeySecret: config.AccessKeySecret, + ResourceGroupId: config.ResourceGroupId, + Region: lo. + If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). + Else("ap-southeast-1"), + }) + if err != nil { + return nil, fmt.Errorf("could not create certmgr: %w", err) + } + return &Deployer{ - config: config, - logger: slog.Default(), - sdkClient: client, + config: config, + logger: slog.Default(), + sdkClient: client, + sdkCertmgr: pcertmgr, }, nil } @@ -63,6 +79,8 @@ func (d *Deployer) SetLogger(logger *slog.Logger) { } else { d.logger = logger } + + d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) { @@ -73,57 +91,45 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep return nil, fmt.Errorf("config `domain` is required") } + // 上传证书 + upres, err := d.sdkCertmgr.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/oss/developer-reference/putcname - putCnameReq := &oss.PutCnameRequest{ - Bucket: tea.String(d.config.Bucket), - BucketCnameConfiguration: &oss.BucketCnameConfiguration{ + putBucketCnameReq := &osssdk.PutCnameRequest{ + Cname: &osssdk.PutCnameRequestCname{ Domain: tea.String(d.config.Domain), - CertificateConfiguration: &oss.CertificateConfiguration{ + CertificateConfiguration: &osssdk.PutCnameRequestCnameCertificateConfiguration{ + CertId: tea.String(upres.ExtendedData["CertIdentifier"].(string)), Certificate: tea.String(certPEM), PrivateKey: tea.String(privkeyPEM), Force: tea.Bool(true), }, }, } - putCnameResp, err := d.sdkClient.PutCname(ctx, putCnameReq) - d.logger.Debug("sdk request 'oss.PutCname'", slog.Any("request", putCnameReq), slog.Any("response", putCnameResp)) + putBucketCnameResp, err := d.sdkClient.PutBucketCnameWithContext(ctx, putBucketCnameReq) + d.logger.Debug("sdk request 'oss.PutBucketCname'", slog.String("params.bucket", d.config.Bucket), slog.Any("request", putBucketCnameReq), slog.Any("response", putBucketCnameResp)) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'oss.PutCname': %w", err) + return nil, fmt.Errorf("failed to execute sdk request 'oss.PutBucketCname': %w", err) } return &DeployResult{}, 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) +func createSDKClient(accessKeyId, accessKeySecret, region, bucket string) (*osssdk.Client, error) { + client, err := osssdk.NewClient("", + osssdk.WithAkSk(accessKeyId, accessKeySecret), + osssdk.WithRegion(region), + osssdk.WithBucket(bucket), + ) + if err != nil { + return nil, err } - provider := credentials.NewStaticCredentialsProvider(accessKeyId, accessKeySecret) - config := oss.LoadDefaultConfig(). - WithCredentialsProvider(provider). - WithEndpoint(endpoint) - if region != "" { - config = config.WithRegion(region) - } - - client := oss.NewClient(config) return client, nil } diff --git a/pkg/core/deployer/providers/huaweicloud-obs/huaweicloud_obs.go b/pkg/core/deployer/providers/huaweicloud-obs/huaweicloud_obs.go index ff6b3c717..12bd01fa4 100644 --- a/pkg/core/deployer/providers/huaweicloud-obs/huaweicloud_obs.go +++ b/pkg/core/deployer/providers/huaweicloud-obs/huaweicloud_obs.go @@ -105,7 +105,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep CertificateChain: certPEM, PrivateKey: privkeyPEM, } - putBucketCustomDomainResp, err := d.sdkClient.PutBucketCustomDomainWithContext(ctx, d.config.Bucket, putBucketCustomDomainReq) + putBucketCustomDomainResp, err := d.sdkClient.PutBucketCustomDomainWithContext(ctx, putBucketCustomDomainReq) d.logger.Debug("sdk request 'obs.PutBucketCustomDomain'", slog.String("params.bucket", d.config.Bucket), slog.String("params.customdomain", d.config.Domain), slog.Any("request", putBucketCustomDomainReq), slog.Any("response", putBucketCustomDomainResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'obs.PutBucketCustomDomain': %w", err) diff --git a/pkg/sdk3rd/alibabacloud/oss/api_put_bucket_cname.go b/pkg/sdk3rd/alibabacloud/oss/api_put_bucket_cname.go new file mode 100644 index 000000000..8865f2b27 --- /dev/null +++ b/pkg/sdk3rd/alibabacloud/oss/api_put_bucket_cname.go @@ -0,0 +1,51 @@ +package oss + +import ( + "context" + "encoding/xml" + "fmt" + "net/http" +) + +type PutCnameRequest struct { + XMLName xml.Name `json:"-" xml:"BucketCnameConfiguration"` + Cname *PutCnameRequestCname `json:",omitempty" xml:"Cname,omitempty"` +} + +type PutCnameRequestCname struct { + Domain *string `json:",omitempty" xml:"Domain,omitempty"` + CertificateConfiguration *PutCnameRequestCnameCertificateConfiguration `json:",omitempty" xml:"CertificateConfiguration,omitempty"` +} + +type PutCnameRequestCnameCertificateConfiguration struct { + CertId *string `json:",omitempty" xml:"CertId,omitempty"` + Certificate *string `json:",omitempty" xml:"Certificate,omitempty"` + PrivateKey *string `json:",omitempty" xml:"PrivateKey,omitempty"` + PreviousCertId *string `json:",omitempty" xml:"PreviousCertId,omitempty"` + Force *bool `json:",omitempty" xml:"Force,omitempty"` + DeleteCertificate *bool `json:",omitempty" xml:"DeleteCertificate,omitempty"` +} + +type PutCnameResponse struct { + sdkResponseBase +} + +func (c *Client) PutBucketCname(req *PutCnameRequest) (*PutCnameResponse, error) { + return c.PutBucketCnameWithContext(context.Background(), req) +} + +func (c *Client) PutBucketCnameWithContext(ctx context.Context, req *PutCnameRequest) (*PutCnameResponse, error) { + httpreq, err := c.newRequest(http.MethodPost, "/?cname&comp=add", fmt.Sprintf("/%s/", c.bucket), req) + if err != nil { + return nil, err + } else { + httpreq.SetContext(ctx) + } + + result := &PutCnameResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/alibabacloud/oss/client.go b/pkg/sdk3rd/alibabacloud/oss/client.go new file mode 100644 index 000000000..a7a584d82 --- /dev/null +++ b/pkg/sdk3rd/alibabacloud/oss/client.go @@ -0,0 +1,296 @@ +package oss + +import ( + "bytes" + "crypto/hmac" + "crypto/md5" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/xml" + "fmt" + "hash" + "net/http" + "net/url" + "sort" + "strings" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" +) + +type Client struct { + bucket string + + rc *resty.Client +} + +func NewClient(endpoint string, optFns ...OptionsFunc) (*Client, error) { + opts := &Options{} + for _, fn := range optFns { + fn(opts) + } + + if opts.Region == "" { + return nil, fmt.Errorf("sdkerr: unset region") + } + if opts.AccessKeyId == "" { + return nil, fmt.Errorf("sdkerr: unset accessKeyId") + } + if opts.AccessKeySecret == "" { + return nil, fmt.Errorf("sdkerr: unset secretAccessKey") + } + + if endpoint == "" { + if opts.Bucket == "" { + endpoint = fmt.Sprintf("https://oss-%s.aliyuncs.com", url.PathEscape(opts.Region)) + } else { + endpoint = fmt.Sprintf("https://%s.oss-%s.aliyuncs.com", url.PathEscape(opts.Bucket), url.PathEscape(opts.Region)) + } + } else { + if baseUrl, err := url.Parse(endpoint); err != nil { + return nil, fmt.Errorf("sdkerr: invalid endpoint: %w", err) + } else if baseUrl.Scheme == "" { + endpoint = "https://" + endpoint + } + } + + restyClient := resty.New(). + SetBaseURL(endpoint). + SetHeader("User-Agent", app.AppUserAgent). + SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + // API 签名机制: + // https://help.aliyun.com/zh/oss/developer-reference/recommend-to-use-signature-version-4 + // https://www.alibabacloud.com/help/en/oss/developer-reference/recommend-to-use-signature-version-4 + + method := strings.ToUpper(req.Method) + + nowUtc := time.Now().UTC() + headerDateStr := nowUtc.Format(http.TimeFormat) + requestDateStr := nowUtc.Format("20060102T150405Z") + signDateStr := nowUtc.Format("20060102") + + requestResStr := req.Header.Get("X-API-Resource") + req.Header.Del("X-API-Resource") + + canonicalUrl := escapePath(req.URL.Path) + if canonicalUrl == "" { + canonicalUrl = "/" + } + if canonicalUrl == "/" && requestResStr != "" { + canonicalUrl = escapePath(requestResStr) + } + + canonicalQueryStr := "" + if len(req.URL.Query()) > 0 { + query := req.URL.Query() + + keys := make([]string, 0, len(query)) + for key := range query { + keys = append(keys, key) + } + sort.Strings(keys) + + for i, key := range keys { + if i > 0 { + canonicalQueryStr += "&" + } + + value := query.Get(key) + if value == "" { + canonicalQueryStr += escapeQuery(key) + } else { + canonicalQueryStr += escapeQuery(key) + "=" + escapeQuery(value) + } + } + } + + canonicalHeaders := "" + additionalHeaders := "" + if len(req.Header) > 0 { + if req.Header.Get("X-OSS-Date") == "" { + req.Header.Set("X-OSS-Date", requestDateStr) + } + if req.Header.Get("X-OSS-Content-SHA256") == "" { + req.Header.Set("X-OSS-Content-SHA256", "UNSIGNED-PAYLOAD") + } + + keys := make([]string, 0, len(req.Header)) + for key := range req.Header { + key = strings.ToLower(key) + if strings.HasPrefix(key, "x-oss-") { + keys = append(keys, key) + } + if key == "content-type" || key == "content-md5" { + keys = append(keys, key) + } + } + sort.Strings(keys) + + for i, key := range keys { + if i > 0 { + canonicalHeaders += "\n" + } + + value := strings.TrimSpace(req.Header.Get(key)) + canonicalHeaders += key + ":" + value + } + + canonicalHeaders += "\n" + } + + hashedPayload := req.Header.Get("X-OSS-Content-SHA256") + + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, canonicalUrl, canonicalQueryStr, canonicalHeaders, additionalHeaders, hashedPayload) + canonicalRequestHash := sha256.Sum256([]byte(canonicalRequest)) + canonicalRequestHashHex := strings.ToLower(hex.EncodeToString(canonicalRequestHash[:])) + + const signAlgorithmHeader = "OSS4-HMAC-SHA256" + scope := fmt.Sprintf("%s/%s/oss/aliyun_v4_request", signDateStr, opts.Region) + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", signAlgorithmHeader, requestDateStr, scope, canonicalRequestHashHex) + + var h hash.Hash + h = hmac.New(sha256.New, []byte("aliyun_v4"+opts.AccessKeySecret)) + h.Write([]byte(signDateStr)) + kDate := h.Sum(nil) + h = hmac.New(sha256.New, kDate) + h.Write([]byte(opts.Region)) + kRegion := h.Sum(nil) + h = hmac.New(sha256.New, kRegion) + h.Write([]byte("oss")) + kService := h.Sum(nil) + h = hmac.New(sha256.New, kService) + h.Write([]byte("aliyun_v4_request")) + kSigning := h.Sum(nil) + + h = hmac.New(sha256.New, kSigning) + h.Write([]byte(stringToSign)) + signature := strings.ToLower(hex.EncodeToString(h.Sum(nil))) + + req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s/%s, Signature=%s", signAlgorithmHeader, opts.AccessKeyId, scope, signature)) + req.Header.Set("Date", headerDateStr) + + return nil + }) + + return &Client{ + bucket: opts.Bucket, + rc: restyClient, + }, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(method string, path string, resource string, params any) (*resty.Request, error) { + if method == "" { + return nil, fmt.Errorf("sdkerr: unset method") + } + if path == "" { + return nil, fmt.Errorf("sdkerr: unset path") + } + + requestUrl, err := url.Parse(path) + if err != nil { + return nil, fmt.Errorf("sdkerr: invalid path: %w", err) + } else if requestUrl.IsAbs() { + return nil, fmt.Errorf("sdkerr: path should be relative") + } + + payloadStr := "" + contentType := "" + if params != nil { + // 目前仅支持 XML 请求体,仅适用于非 S3 兼容接口 + payloadb, err := xml.Marshal(params) + if err != nil { + return nil, err + } + + payloadStr = string(payloadb) + contentType = "application/xml" + } + payloadMd5 := md5.Sum([]byte(payloadStr)) + payloadMd5Encoded := base64.StdEncoding.EncodeToString(payloadMd5[:]) + + req := c.rc.R() + req.Method = method + req.URL = requestUrl.Path + req.QueryParam = requestUrl.Query() + req.SetHeader("Content-MD5", payloadMd5Encoded) + req.SetHeader("Content-Type", contentType) + req.SetHeader("X-API-Resource", resource) + req.SetBody(payloadStr) + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + // WARN: + // PLEASE DO NOT USE `req.SetBody` HERE! USE `newRequest` INSTEAD. + // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + xml.Unmarshal(resp.Body(), &res) + } + return resp, err + } + + if len(resp.Body()) != 0 { + if err := xml.Unmarshal(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } + } + + return resp, nil +} + +func escapeQuery(str string) string { + res := url.QueryEscape(str) + res = strings.ReplaceAll(res, "+", "%20") + return res +} + +func escapePath(path string) string { + var buf bytes.Buffer + for i := 0; i < len(path); i++ { + c := path[i] + noEscape := (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || + c == '.' || + c == '_' || + c == '~' || + c == '/' + if noEscape { + buf.WriteByte(c) + } else { + fmt.Fprintf(&buf, "%%%02X", c) + } + } + return buf.String() +} diff --git a/pkg/sdk3rd/alibabacloud/oss/options.go b/pkg/sdk3rd/alibabacloud/oss/options.go new file mode 100644 index 000000000..69fad1124 --- /dev/null +++ b/pkg/sdk3rd/alibabacloud/oss/options.go @@ -0,0 +1,29 @@ +package oss + +type Options struct { + AccessKeyId string + AccessKeySecret string + Region string + Bucket string +} + +type OptionsFunc func(*Options) + +func WithAkSk(ak, sk string) OptionsFunc { + return func(o *Options) { + o.AccessKeyId = ak + o.AccessKeySecret = sk + } +} + +func WithRegion(region string) OptionsFunc { + return func(o *Options) { + o.Region = region + } +} + +func WithBucket(bucket string) OptionsFunc { + return func(o *Options) { + o.Bucket = bucket + } +} diff --git a/pkg/sdk3rd/alibabacloud/oss/types.go b/pkg/sdk3rd/alibabacloud/oss/types.go new file mode 100644 index 000000000..fa88d3cf7 --- /dev/null +++ b/pkg/sdk3rd/alibabacloud/oss/types.go @@ -0,0 +1,7 @@ +package oss + +type sdkResponse interface{} + +type sdkResponseBase struct{} + +var _ sdkResponse = (*sdkResponseBase)(nil) diff --git a/pkg/sdk3rd/huaweicloud/obs/api_put_bucket_customdomain.go b/pkg/sdk3rd/huaweicloud/obs/api_put_bucket_customdomain.go index 5cc2d39d9..79aeb8882 100644 --- a/pkg/sdk3rd/huaweicloud/obs/api_put_bucket_customdomain.go +++ b/pkg/sdk3rd/huaweicloud/obs/api_put_bucket_customdomain.go @@ -22,14 +22,11 @@ type PutBucketCustomDomainResponse struct { sdkResponseBase } -func (c *Client) PutBucketCustomDomain(bucket string, req *PutBucketCustomDomainRequest) (*PutBucketCustomDomainResponse, error) { - return c.PutBucketCustomDomainWithContext(context.Background(), bucket, req) +func (c *Client) PutBucketCustomDomain(req *PutBucketCustomDomainRequest) (*PutBucketCustomDomainResponse, error) { + return c.PutBucketCustomDomainWithContext(context.Background(), req) } -func (c *Client) PutBucketCustomDomainWithContext(ctx context.Context, bucket string, req *PutBucketCustomDomainRequest) (*PutBucketCustomDomainResponse, error) { - if bucket == "" { - return nil, fmt.Errorf("sdkerr: bad request: unset bucket") - } +func (c *Client) PutBucketCustomDomainWithContext(ctx context.Context, req *PutBucketCustomDomainRequest) (*PutBucketCustomDomainResponse, error) { if req.CustomDomain == "" { return nil, fmt.Errorf("sdkerr: bad request: unset customdomain") } diff --git a/pkg/sdk3rd/huaweicloud/obs/client.go b/pkg/sdk3rd/huaweicloud/obs/client.go index 09a9b3bec..4ef6f4eef 100644 --- a/pkg/sdk3rd/huaweicloud/obs/client.go +++ b/pkg/sdk3rd/huaweicloud/obs/client.go @@ -1,6 +1,7 @@ package obs import ( + "bytes" "crypto/hmac" "crypto/md5" "crypto/sha1" @@ -72,16 +73,16 @@ func NewClient(endpoint string, optFns ...OptionsFunc) (*Client, error) { for _, key := range keys { value := strings.TrimSpace(req.Header.Get(key)) - canonicalizedHeaders += fmt.Sprintf("%s:%s", key, url.QueryEscape(value)) + canonicalizedHeaders += key + ":" + escapeQuery(value) canonicalizedHeaders += "\n" } } bucketName := opts.Bucket objectName := strings.Trim(req.URL.Path, "/") - canonicalizedResource := fmt.Sprintf("/%s/%s", bucketName, objectName) + canonicalizedResources := escapePath(fmt.Sprintf("/%s/%s", bucketName, objectName)) if bucketName == "" && objectName == "" { - canonicalizedResource = "/" + canonicalizedResources = "/" } if len(req.URL.Query()) > 0 { query := req.URL.Query() @@ -94,16 +95,16 @@ func NewClient(endpoint string, optFns ...OptionsFunc) (*Client, error) { for i, key := range keys { if i == 0 { - canonicalizedResource += "?" + canonicalizedResources += "?" } else { - canonicalizedResource += "&" + canonicalizedResources += "&" } value := query.Get(key) if value == "" { - canonicalizedResource += key + canonicalizedResources += escapeQuery(key) } else { - canonicalizedResource += fmt.Sprintf("%s=%s", strings.ToLower(key), url.QueryEscape(value)) + canonicalizedResources += escapeQuery(key) + "=" + escapeQuery(value) } } } @@ -115,8 +116,7 @@ func NewClient(endpoint string, optFns ...OptionsFunc) (*Client, error) { contentMd5 := req.Header.Get("Content-MD5") contentType := req.Header.Get("Content-Type") - stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s%s", method, contentMd5, contentType, dateStr, canonicalizedHeaders, canonicalizedResource) - println("stringToSign:", stringToSign) + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s%s", method, contentMd5, contentType, dateStr, canonicalizedHeaders, canonicalizedResources) h := hmac.New(sha1.New, []byte(opts.SecretAccessKey)) h.Write([]byte(stringToSign)) @@ -216,3 +216,32 @@ func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*rest return resp, nil } + +func escapeQuery(str string) string { + res := url.QueryEscape(str) + res = strings.ReplaceAll(res, "%7E", "~") + res = strings.ReplaceAll(res, "%2F", "/") + res = strings.ReplaceAll(res, "%20", "+") + return res +} + +func escapePath(path string) string { + var buf bytes.Buffer + for i := 0; i < len(path); i++ { + c := path[i] + noEscape := (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || + c == '.' || + c == '_' || + c == '~' || + c == '/' + if noEscape { + buf.WriteByte(c) + } else { + fmt.Fprintf(&buf, "%%%02X", c) + } + } + return buf.String() +} diff --git a/pkg/sdk3rd/volcengine/tos/client.go b/pkg/sdk3rd/volcengine/tos/client.go index 699754367..17ee1d4e0 100644 --- a/pkg/sdk3rd/volcengine/tos/client.go +++ b/pkg/sdk3rd/volcengine/tos/client.go @@ -1,6 +1,7 @@ package tos import ( + "bytes" "crypto/hmac" "crypto/md5" "crypto/sha256" @@ -69,7 +70,7 @@ func NewClient(endpoint string, optFns ...OptionsFunc) (*Client, error) { requestDateStr := nowUtc.Format("20060102T150405Z") credentialDateStr := nowUtc.Format("20060102") - canonicalUrl := req.URL.Path + canonicalUrl := escapePath(req.URL.Path) if canonicalUrl == "" { canonicalUrl = "/" } @@ -90,7 +91,7 @@ func NewClient(endpoint string, optFns ...OptionsFunc) (*Client, error) { } value := query.Get(key) - canonicalQueryStr += fmt.Sprintf("%s=%s", url.QueryEscape(key), url.QueryEscape(value)) + canonicalQueryStr += escapeQuery(key) + "=" + escapeQuery(value) } } @@ -123,20 +124,20 @@ func NewClient(endpoint string, optFns ...OptionsFunc) (*Client, error) { } value := strings.TrimSpace(req.Header.Get(key)) - canonicalHeaders += fmt.Sprintf("%s:%s", key, value) + canonicalHeaders += key + ":" + value signedHeaders += key } canonicalHeaders += "\n" } - payloadSha256Str := req.Header.Get("X-TOS-Content-SHA256") - if payloadSha256Str == "" { - payloadSha256 := sha256.Sum256([]byte{}) - payloadSha256Str = strings.ToLower(hex.EncodeToString(payloadSha256[:])) + hashedPayload := req.Header.Get("X-TOS-Content-SHA256") + if hashedPayload == "" { + temp := sha256.Sum256([]byte{}) + hashedPayload = strings.ToLower(hex.EncodeToString(temp[:])) } - canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, canonicalUrl, canonicalQueryStr, canonicalHeaders, signedHeaders, payloadSha256Str) + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, canonicalUrl, canonicalQueryStr, canonicalHeaders, signedHeaders, hashedPayload) canonicalRequestHash := sha256.Sum256([]byte(canonicalRequest)) canonicalRequestHashHex := strings.ToLower(hex.EncodeToString(canonicalRequestHash[:])) @@ -256,3 +257,30 @@ func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*rest return resp, nil } + +func escapeQuery(str string) string { + res := url.QueryEscape(str) + res = strings.ReplaceAll(res, "+", "%20") + return res +} + +func escapePath(path string) string { + var buf bytes.Buffer + for i := 0; i < len(path); i++ { + c := path[i] + noEscape := (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || + c == '.' || + c == '_' || + c == '~' || + c == '/' + if noEscape { + buf.WriteByte(c) + } else { + fmt.Fprintf(&buf, "%%%02X", c) + } + } + return buf.String() +}