package oss import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "hash" "io" "strings" "time" "lone-services/pkg/utils" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" sts20150401 "github.com/alibabacloud-go/sts-20150401/v2/client" util "github.com/alibabacloud-go/tea-utils/v2/service" "github.com/alibabacloud-go/tea/tea" ) const ossProduct = "oss" type Config struct { AccessKeyId string AccessKeySecret string RoleArn string Region string BucketName string Endpoint string StsEndpoint string Dir string ExpireSeconds int64 CallbackUrl string RoleSessionName string } type Credentials struct { AccessKeyId string AccessKeySecret string SecurityToken string Expiration string } type UploadPolicy struct { TaskId string `json:"task_id"` Policy string `json:"policy"` SecurityToken string `json:"security_token"` SignatureVersion string `json:"x_oss_signature_version"` Credential string `json:"x_oss_credential"` Date string `json:"x_oss_date"` Signature string `json:"signature"` Host string `json:"host"` Dir string `json:"dir"` DateDir string `json:"date_dir"` Callback string `json:"callback"` CallbackVar string `json:"callback_var"` ExpireSeconds int64 `json:"expire_seconds"` } func NewConfig(roleSessionName string) *Config { dir := utils.GetConfigString("oss.dir") if dir != utils.StringEmpty && !strings.HasSuffix(dir, "/") { dir += "/" } expire := int64(utils.GetConfigInt("oss.expireSeconds")) if expire <= 0 { expire = 3600 } region := utils.GetConfigString("oss.region") if region == utils.StringEmpty { region = "oss-cn-hangzhou" } stsEndpoint := utils.GetConfigString("oss.stsEndpoint") if stsEndpoint == utils.StringEmpty { stsEndpoint = "sts." + strings.TrimPrefix(region, "oss-") + ".aliyuncs.com" } endpoint := utils.GetConfigString("oss.endpoint") if endpoint == utils.StringEmpty { endpoint = "https://oss-accelerate.aliyuncs.com" } bucket := utils.GetConfigString("oss.bucketName") if bucket == utils.StringEmpty { bucket = "lone-images" } return &Config{ AccessKeyId: utils.GetConfigString("oss.accessKeyId"), AccessKeySecret: utils.GetConfigString("oss.accessKeySecret"), RoleArn: utils.GetConfigString("oss.roleArn"), Region: region, BucketName: bucket, Endpoint: endpoint, StsEndpoint: stsEndpoint, Dir: dir, ExpireSeconds: expire, CallbackUrl: utils.GetConfigString("oss.callbackUrl"), RoleSessionName: roleSessionName, } } func (c *Config) Validate() error { if c.AccessKeyId == utils.StringEmpty || c.AccessKeySecret == utils.StringEmpty || c.RoleArn == utils.StringEmpty { return fmt.Errorf("oss policy config incomplete") } if c.CallbackUrl == utils.StringEmpty { return fmt.Errorf("oss callbackUrl empty") } if c.BucketName == utils.StringEmpty { return fmt.Errorf("oss bucketName empty") } return nil } func AssumeRole(cfg *Config) (*Credentials, error) { client, err := sts20150401.NewClient(&openapi.Config{ AccessKeyId: tea.String(cfg.AccessKeyId), AccessKeySecret: tea.String(cfg.AccessKeySecret), Endpoint: tea.String(cfg.StsEndpoint), }) if err != nil { return nil, fmt.Errorf("create sts client: %w", err) } response, err := client.AssumeRoleWithOptions(&sts20150401.AssumeRoleRequest{ DurationSeconds: tea.Int64(cfg.ExpireSeconds), RoleArn: tea.String(cfg.RoleArn), RoleSessionName: tea.String(cfg.RoleSessionName), }, &util.RuntimeOptions{}) if err != nil { return nil, fmt.Errorf("assume role: %w", err) } if response == nil || response.Body == nil || response.Body.Credentials == nil { return nil, fmt.Errorf("assume role: empty credentials") } cred := response.Body.Credentials return &Credentials{ AccessKeyId: tea.StringValue(cred.AccessKeyId), AccessKeySecret: tea.StringValue(cred.AccessKeySecret), SecurityToken: tea.StringValue(cred.SecurityToken), Expiration: tea.StringValue(cred.Expiration), }, nil } func BuildUploadPolicy(cfg *Config, taskId string, cred *Credentials) (*UploadPolicy, error) { region := strings.TrimPrefix(strings.TrimSpace(cfg.Region), "oss-") dateDir := time.Now().In(utils.AsiaShanghai).Format("20060102") objectPrefix := fmt.Sprintf("%s%s/%s/", cfg.Dir, dateDir, taskId) utcTime := time.Now().UTC() date := utcTime.Format("20060102") ossDate := utcTime.Format("20060102T150405Z") expiration := utcTime.Add(time.Duration(cfg.ExpireSeconds) * time.Second) credential := fmt.Sprintf("%s/%s/%s/%s/aliyun_v4_request", cred.AccessKeyId, date, region, ossProduct) policyMap := map[string]any{ "expiration": expiration.Format("2006-01-02T15:04:05.000Z"), "conditions": []any{ map[string]string{"bucket": cfg.BucketName}, map[string]string{"x-oss-signature-version": "OSS4-HMAC-SHA256"}, map[string]string{"x-oss-credential": credential}, map[string]string{"x-oss-date": ossDate}, map[string]string{"x-oss-security-token": cred.SecurityToken}, []any{"starts-with", "$key", objectPrefix}, []any{"content-length-range", 1, 512 * 1024 * 1024}, }, } policyJSON, err := json.Marshal(policyMap) if err != nil { return nil, fmt.Errorf("marshal policy: %w", err) } policyBase64 := base64.StdEncoding.EncodeToString(policyJSON) signature, err := signPolicyV4(cred.AccessKeySecret, date, region, policyBase64) if err != nil { return nil, err } callbackBody := `{"bucket":${bucket},"object":${object},"etag":${etag},"size":${size},"mimeType":${mimeType},"task_id":${x:task_id}}` callbackVars := map[string]string{"x:task_id": taskId} return &UploadPolicy{ TaskId: taskId, Policy: policyBase64, SecurityToken: cred.SecurityToken, SignatureVersion: "OSS4-HMAC-SHA256", Credential: credential, Date: ossDate, Signature: signature, Host: cfg.Endpoint, Dir: objectPrefix, DateDir: dateDir, Callback: encodeCallbackBase64(cfg.CallbackUrl, callbackBody, "application/json"), CallbackVar: encodeCallbackVarBase64(callbackVars), ExpireSeconds: cfg.ExpireSeconds, }, nil } func signPolicyV4(accessKeySecret, date, region, policyBase64 string) (string, error) { hmacHash := func() hash.Hash { return sha256.New() } h1 := hmac.New(hmacHash, []byte("aliyun_v4"+accessKeySecret)) if _, err := io.WriteString(h1, date); err != nil { return "", err } h2 := hmac.New(hmacHash, h1.Sum(nil)) if _, err := io.WriteString(h2, region); err != nil { return "", err } h3 := hmac.New(hmacHash, h2.Sum(nil)) if _, err := io.WriteString(h3, ossProduct); err != nil { return "", err } h4 := hmac.New(hmacHash, h3.Sum(nil)) if _, err := io.WriteString(h4, "aliyun_v4_request"); err != nil { return "", err } h := hmac.New(hmacHash, h4.Sum(nil)) if _, err := io.WriteString(h, policyBase64); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } type callbackParam struct { CallbackUrl string `json:"callbackUrl"` CallbackBody string `json:"callbackBody"` CallbackBodyType string `json:"callbackBodyType"` } func encodeCallbackBase64(url, body, bodyType string) string { raw, err := json.Marshal(callbackParam{ CallbackUrl: url, CallbackBody: body, CallbackBodyType: bodyType, }) if err != nil { return "" } return base64.StdEncoding.EncodeToString(raw) } func encodeCallbackVarBase64(vars map[string]string) string { if len(vars) == 0 { return "" } raw, err := json.Marshal(vars) if err != nil { return "" } return base64.StdEncoding.EncodeToString(raw) }