141 lines
5.8 KiB
Go
141 lines
5.8 KiB
Go
package oss
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"mime"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.yuntuo.hk/k2safe/go-omni/storage"
|
|
sdk "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
|
|
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
|
|
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/signer"
|
|
)
|
|
|
|
// Recompute signatures independently from the SDK using fixed test credentials.
|
|
// This catches response parameters appended after signing, not just their presence.
|
|
func validReadSignature(u *url.URL, version sdk.SignatureVersionType) bool {
|
|
q := u.Query()
|
|
if version == sdk.SignatureVersionV1 {
|
|
var keys []string
|
|
for key := range q {
|
|
if strings.HasPrefix(key, "response-") {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
var params []string
|
|
for _, key := range keys {
|
|
params = append(params, key+"="+q.Get(key))
|
|
}
|
|
message := "GET\n\n\n" + q.Get("Expires") + "\n/testbucket/folder/file.pdf?" + strings.Join(params, "&")
|
|
mac := hmac.New(sha1.New, []byte("fixtureSecret"))
|
|
mac.Write([]byte(message))
|
|
return hmac.Equal([]byte(q.Get("Signature")), []byte(base64.StdEncoding.EncodeToString(mac.Sum(nil))))
|
|
}
|
|
signature := q.Get("x-oss-signature")
|
|
q.Del("x-oss-signature")
|
|
canonical := "GET\n/testbucket/folder/file.pdf\n" + strings.ReplaceAll(q.Encode(), "+", "%20") + "\n\n\nUNSIGNED-PAYLOAD"
|
|
hash := sha256.Sum256([]byte(canonical))
|
|
_, scope, _ := strings.Cut(q.Get("x-oss-credential"), "/")
|
|
message := "OSS4-HMAC-SHA256\n" + q.Get("x-oss-date") + "\n" + scope + "\n" + hex.EncodeToString(hash[:])
|
|
key := []byte("aliyun_v4fixtureSecret")
|
|
for _, part := range strings.Split(scope, "/") {
|
|
mac := hmac.New(sha256.New, key)
|
|
mac.Write([]byte(part))
|
|
key = mac.Sum(nil)
|
|
}
|
|
mac := hmac.New(sha256.New, key)
|
|
mac.Write([]byte(message))
|
|
return hmac.Equal([]byte(signature), []byte(hex.EncodeToString(mac.Sum(nil))))
|
|
}
|
|
|
|
func TestReadOptionsNativeSignatures(t *testing.T) {
|
|
for name, version := range map[string]sdk.SignatureVersionType{"v1": sdk.SignatureVersionV1, "v4": sdk.SignatureVersionV4} {
|
|
t.Run(name, func(t *testing.T) {
|
|
cfg := sdk.LoadDefaultConfig().WithRegion("cn-hangzhou").WithEndpoint("https://files.example").WithUseCName(true).WithSignatureVersion(version).WithCredentialsProvider(credentials.NewStaticCredentialsProvider("fixtureAccess", "fixtureSecret"))
|
|
store, err := New(sdk.NewClient(cfg), "testbucket", 64)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, options := range []storage.ReadOptions{{}, {Disposition: storage.DispositionInline, FileName: "报告 2026.pdf", ContentType: "application/pdf"}, {Disposition: storage.DispositionAttachment, FileName: "download.pdf"}} {
|
|
before := time.Now().UTC().Truncate(time.Second)
|
|
link, err := store.PresignGetWithOptions(context.Background(), "folder/file.pdf", 30*time.Minute, options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
u, err := url.Parse(link.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
q := u.Query()
|
|
if u.Host != "files.example" || u.Path != "/folder/file.pdf" || !validReadSignature(u, version) {
|
|
t.Fatal("SDK response overrides were not signed")
|
|
}
|
|
disposition, params, err := mime.ParseMediaType(q.Get("response-content-disposition"))
|
|
want := options.Disposition
|
|
if want == "" {
|
|
want = "attachment"
|
|
}
|
|
if err != nil || disposition != want || params["filename"] != options.FileName || q.Get("response-content-type") != options.ContentType || q.Get("response-cache-control") != "private, no-store" {
|
|
t.Fatal("wrong signed headers", err)
|
|
}
|
|
if link.ExpiresAt.Before(before.Add(30*time.Minute)) || link.ExpiresAt.After(time.Now().Add(30*time.Minute)) {
|
|
t.Fatal("expiry drift")
|
|
}
|
|
var expires time.Time
|
|
if version == sdk.SignatureVersionV4 {
|
|
start, _ := time.Parse("20060102T150405Z", q.Get("x-oss-date"))
|
|
seconds, _ := strconv.Atoi(q.Get("x-oss-expires"))
|
|
expires = start.Add(time.Duration(seconds) * time.Second)
|
|
} else {
|
|
seconds, _ := strconv.ParseInt(q.Get("Expires"), 10, 64)
|
|
expires = time.Unix(seconds, 0)
|
|
}
|
|
if !expires.Equal(link.ExpiresAt) {
|
|
t.Fatal("result does not match signature expiry")
|
|
}
|
|
for _, name := range []string{"response-content-disposition", "response-content-type", "response-cache-control"} {
|
|
changed := *u
|
|
query := changed.Query()
|
|
query.Set(name, "changed")
|
|
changed.RawQuery = query.Encode()
|
|
if validReadSignature(&changed, version) {
|
|
t.Fatal("unsigned override", name)
|
|
}
|
|
}
|
|
}
|
|
for _, ttl := range []time.Duration{0, time.Second - time.Nanosecond, time.Hour + time.Nanosecond} {
|
|
if _, err := store.PresignGetWithOptions(context.Background(), "file", ttl, storage.ReadOptions{}); err == nil {
|
|
t.Fatal("TTL accepted")
|
|
}
|
|
}
|
|
for _, options := range []storage.ReadOptions{{FileName: "x\r\nY: z"}, {ContentType: "text/plain\r\nY: z"}, {Disposition: "bad"}} {
|
|
if _, err := store.PresignGetWithOptions(context.Background(), "file", time.Minute, options); err == nil {
|
|
t.Fatal("invalid header accepted")
|
|
}
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, err := store.PresignGetWithOptions(ctx, "file", time.Minute, storage.ReadOptions{}); err != context.Canceled {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
// A caller-supplied unsigned SDK configuration cannot masquerade as a link.
|
|
client := sdk.NewClient(sdk.LoadDefaultConfig().WithRegion("cn-hangzhou").WithEndpoint("https://files.example").WithCredentialsProvider(credentials.NewStaticCredentialsProvider("fixtureAccess", "fixtureSecret")), func(o *sdk.Options) { o.Signer = &signer.NopSigner{} })
|
|
store, _ := New(client, "testbucket", 64)
|
|
if _, err := store.PresignGetWithOptions(context.Background(), "file", time.Minute, storage.ReadOptions{}); err == nil {
|
|
t.Fatal("unsigned URL accepted")
|
|
}
|
|
}
|