Files
2026-10-21 13:29:03 +08:00

255 lines
7.7 KiB
Go

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)
}