Compare commits
1
Commits
master
...
fix-s3-alibaba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d07036b92a |
No files matched your search
@@ -0,0 +1,384 @@
|
||||
// OSS V4 signing (OSS4-HMAC-SHA256) for Alibaba Cloud OSS
|
||||
//
|
||||
// Implemented from
|
||||
// https://www.alibabacloud.com/help/en/oss/developer-reference/recommend-to-use-signature-version-4
|
||||
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
v4signer "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
ossAlgorithm = "OSS4-HMAC-SHA256"
|
||||
ossUnsignedPayload = "UNSIGNED-PAYLOAD"
|
||||
ossAmzPrefix = "x-amz-"
|
||||
ossPrefix = "x-oss-"
|
||||
)
|
||||
|
||||
// ossV4Signer signs requests with OSS native V4 signatures
|
||||
// (OSS4-HMAC-SHA256) as required by Alibaba Cloud OSS endpoints which
|
||||
// don't accept AWS style signatures.
|
||||
//
|
||||
// Implements the HTTPSignerV4 interface.
|
||||
type ossV4Signer struct {
|
||||
region string // signing region for the credential scope
|
||||
product string // signing product - "oss" or "oss-cloudbox"
|
||||
endpointHost string // host of the endpoint to detect virtual hosted style requests
|
||||
}
|
||||
|
||||
// newOSSV4Signer creates an OSS V4 signer from the options.
|
||||
//
|
||||
// The signing region is derived from the endpoint if possible,
|
||||
// otherwise the region option is used.
|
||||
func newOSSV4Signer(opt *Options) *ossV4Signer {
|
||||
host := ossEndpointHost(opt.Endpoint)
|
||||
region, product := ossRegionProduct(host)
|
||||
if region == "" {
|
||||
region = opt.Region
|
||||
}
|
||||
return &ossV4Signer{
|
||||
region: region,
|
||||
product: product,
|
||||
endpointHost: host,
|
||||
}
|
||||
}
|
||||
|
||||
// ossEndpointHost returns the lower case host part of an endpoint
|
||||
// which may or may not have a scheme, or "" if it can't be parsed.
|
||||
func ossEndpointHost(endpoint string) string {
|
||||
if endpoint == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.Contains(endpoint, "://") {
|
||||
endpoint = "https://" + endpoint
|
||||
}
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.ToLower(u.Hostname())
|
||||
}
|
||||
|
||||
// ossRegionProduct derives the signing region and product from an OSS
|
||||
// endpoint host.
|
||||
//
|
||||
// Standard endpoints look like oss-<region>[-internal].aliyuncs.com
|
||||
// and CloudBox endpoints look like
|
||||
// <cb-id>.<region>.oss-cloudbox[-internal].aliyuncs.com where the
|
||||
// signing region is the CloudBox ID.
|
||||
//
|
||||
// It returns region == "" if the region could not be derived, eg for
|
||||
// accelerate endpoints or custom domains.
|
||||
func ossRegionProduct(host string) (region, product string) {
|
||||
product = "oss"
|
||||
labels := strings.Split(host, ".")
|
||||
for _, label := range labels[1:] {
|
||||
if label == "oss-cloudbox" || label == "oss-cloudbox-internal" {
|
||||
return labels[0], "oss-cloudbox"
|
||||
}
|
||||
}
|
||||
if r, ok := strings.CutPrefix(labels[0], "oss-"); ok {
|
||||
r = strings.TrimSuffix(r, "-internal")
|
||||
if r != "" && !strings.HasPrefix(r, "accelerate") {
|
||||
region = r
|
||||
}
|
||||
}
|
||||
return region, product
|
||||
}
|
||||
|
||||
// ossStorageClassToOSS maps AWS style storage class names (and upper
|
||||
// cased native names as produced by SetTier) to the case sensitive
|
||||
// native OSS names. Native names pass through unchanged.
|
||||
var ossStorageClassToOSS = map[string]string{
|
||||
"STANDARD": "Standard",
|
||||
"STANDARD_IA": "IA",
|
||||
"GLACIER": "Archive",
|
||||
"DEEP_ARCHIVE": "ColdArchive",
|
||||
"ARCHIVE": "Archive",
|
||||
"COLDARCHIVE": "ColdArchive",
|
||||
"DEEPCOLDARCHIVE": "DeepColdArchive",
|
||||
}
|
||||
|
||||
// ossStorageClassFromOSS maps native OSS storage class names back to
|
||||
// the AWS style names the S3 compatible interface uses.
|
||||
var ossStorageClassFromOSS = map[string]string{
|
||||
"Standard": "STANDARD",
|
||||
"IA": "STANDARD_IA",
|
||||
"Archive": "GLACIER",
|
||||
"ColdArchive": "DEEP_ARCHIVE",
|
||||
}
|
||||
|
||||
// ossHeaderRename returns the x-oss-* header equivalent to the
|
||||
// x-amz-* header with the given suffix (the part after "x-amz-",
|
||||
// lower case). hasKey should be true if the request refers to an
|
||||
// object rather than a bucket.
|
||||
//
|
||||
// It returns "" for headers which should be dropped because the
|
||||
// signer sets the x-oss-* equivalent itself.
|
||||
func ossHeaderRename(amzSuffix string, hasKey bool) string {
|
||||
switch amzSuffix {
|
||||
case "date", "content-sha256", "security-token":
|
||||
return ""
|
||||
case "acl":
|
||||
// OSS uses different headers for object and bucket ACLs
|
||||
if hasKey {
|
||||
return "x-oss-object-acl"
|
||||
}
|
||||
return "x-oss-acl"
|
||||
case "server-side-encryption-aws-kms-key-id":
|
||||
return "x-oss-server-side-encryption-key-id"
|
||||
}
|
||||
return ossPrefix + amzSuffix
|
||||
}
|
||||
|
||||
// ossTranslateRequest renames the x-amz-* headers the SDK generates
|
||||
// into the x-oss-* equivalents that OSS uses so that they take part
|
||||
// in the signature. OSS requires all x-oss-* headers sent to be
|
||||
// signed.
|
||||
func ossTranslateRequest(req *http.Request, hasKey bool) {
|
||||
var keys []string
|
||||
for k := range req.Header {
|
||||
if strings.HasPrefix(strings.ToLower(k), ossAmzPrefix) {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
for _, k := range keys {
|
||||
values := req.Header.Values(k)
|
||||
req.Header.Del(k)
|
||||
newKey := ossHeaderRename(strings.ToLower(k)[len(ossAmzPrefix):], hasKey)
|
||||
if newKey == "" {
|
||||
continue
|
||||
}
|
||||
switch newKey {
|
||||
case "x-oss-copy-source":
|
||||
// OSS wants the copy source to start with a "/"
|
||||
for i, v := range values {
|
||||
if !strings.HasPrefix(v, "/") {
|
||||
values[i] = "/" + v
|
||||
}
|
||||
}
|
||||
case "x-oss-storage-class":
|
||||
// OSS uses different case sensitive storage class names
|
||||
for i, v := range values {
|
||||
if native, ok := ossStorageClassToOSS[v]; ok {
|
||||
values[i] = native
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, v := range values {
|
||||
req.Header.Add(newKey, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ossCanonicalURI returns the canonical URI for the request,
|
||||
// expanding a virtual hosted style bucket in the host back to
|
||||
// /bucket/key form.
|
||||
func (s *ossV4Signer) ossCanonicalURI(u *url.URL) string {
|
||||
path := u.EscapedPath()
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
host := strings.ToLower(u.Host)
|
||||
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||
host = h
|
||||
}
|
||||
if s.endpointHost != "" && strings.HasSuffix(host, "."+s.endpointHost) {
|
||||
bucket := strings.TrimSuffix(host, "."+s.endpointHost)
|
||||
path = "/" + bucket + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// ossCanonicalQuery returns the canonical query string for the
|
||||
// request. This signs the query parameters exactly as they are sent
|
||||
// on the wire except that "+" is re-encoded as %20 and the
|
||||
// parameters are sorted. Parameters with an empty value are signed
|
||||
// as a bare name without "=".
|
||||
func ossCanonicalQuery(rawQuery string) string {
|
||||
query := strings.ReplaceAll(rawQuery, "+", "%20")
|
||||
type param struct{ name, value string }
|
||||
var params []param
|
||||
for _, part := range strings.Split(query, "&") {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
name, value, _ := strings.Cut(part, "=")
|
||||
params = append(params, param{name, value})
|
||||
}
|
||||
sort.Slice(params, func(i, j int) bool {
|
||||
if params[i].name != params[j].name {
|
||||
return params[i].name < params[j].name
|
||||
}
|
||||
return params[i].value < params[j].value
|
||||
})
|
||||
var b strings.Builder
|
||||
for i, p := range params {
|
||||
if i > 0 {
|
||||
b.WriteByte('&')
|
||||
}
|
||||
b.WriteString(p.name)
|
||||
if p.value != "" {
|
||||
b.WriteByte('=')
|
||||
b.WriteString(p.value)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ossCanonicalHeaders returns the canonical headers for the request.
|
||||
// OSS signs all x-oss-* headers plus content-type and content-md5.
|
||||
func ossCanonicalHeaders(header http.Header) string {
|
||||
var names []string
|
||||
values := make(map[string]string)
|
||||
for k, vs := range header {
|
||||
name := strings.ToLower(k)
|
||||
if name != "content-type" && name != "content-md5" && !strings.HasPrefix(name, ossPrefix) {
|
||||
continue
|
||||
}
|
||||
trimmed := make([]string, len(vs))
|
||||
for i, v := range vs {
|
||||
trimmed[i] = strings.TrimSpace(v)
|
||||
}
|
||||
names = append(names, name)
|
||||
values[name] = strings.Join(trimmed, ",")
|
||||
}
|
||||
sort.Strings(names)
|
||||
var b strings.Builder
|
||||
for _, name := range names {
|
||||
b.WriteString(name)
|
||||
b.WriteByte(':')
|
||||
b.WriteString(values[name])
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ossHMAC returns the HMAC-SHA256 of value with the given key
|
||||
func ossHMAC(key []byte, value string) []byte {
|
||||
h := hmac.New(sha256.New, key)
|
||||
_, _ = h.Write([]byte(value))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// SignHTTP signs the request with an OSS V4 signature.
|
||||
//
|
||||
// It rewrites the AWS style x-amz-* headers into their x-oss-*
|
||||
// equivalents before signing.
|
||||
//
|
||||
// Implements the HTTPSignerV4 interface.
|
||||
func (s *ossV4Signer) SignHTTP(ctx context.Context, credentials aws.Credentials, req *http.Request, payloadHash string, service string, region string, signingTime time.Time, optFns ...func(*v4signer.SignerOptions)) error {
|
||||
uri := s.ossCanonicalURI(req.URL)
|
||||
// an object request has a path after the /bucket/ prefix
|
||||
hasKey := strings.Contains(strings.Trim(uri, "/"), "/")
|
||||
ossTranslateRequest(req, hasKey)
|
||||
|
||||
utcTime := signingTime.UTC()
|
||||
datetime := utcTime.Format("20060102T150405Z")
|
||||
date := utcTime.Format("20060102")
|
||||
req.Header.Set("Date", utcTime.Format(http.TimeFormat))
|
||||
req.Header.Set("x-oss-date", datetime)
|
||||
// OSS only supports unsigned payloads with V4 signatures
|
||||
req.Header.Set("x-oss-content-sha256", ossUnsignedPayload)
|
||||
if credentials.SessionToken != "" {
|
||||
req.Header.Set("x-oss-security-token", credentials.SessionToken)
|
||||
}
|
||||
|
||||
canonicalRequest := strings.Join([]string{
|
||||
req.Method,
|
||||
uri,
|
||||
ossCanonicalQuery(req.URL.RawQuery),
|
||||
ossCanonicalHeaders(req.Header),
|
||||
"", // no additional signed headers
|
||||
ossUnsignedPayload,
|
||||
}, "\n")
|
||||
|
||||
hash := sha256.Sum256([]byte(canonicalRequest))
|
||||
scope := date + "/" + s.region + "/" + s.product + "/aliyun_v4_request"
|
||||
stringToSign := ossAlgorithm + "\n" + datetime + "\n" + scope + "\n" + hex.EncodeToString(hash[:])
|
||||
|
||||
key := ossHMAC([]byte("aliyun_v4"+credentials.SecretAccessKey), date)
|
||||
key = ossHMAC(key, s.region)
|
||||
key = ossHMAC(key, s.product)
|
||||
key = ossHMAC(key, "aliyun_v4_request")
|
||||
signature := hex.EncodeToString(ossHMAC(key, stringToSign))
|
||||
|
||||
req.Header.Set("Authorization", ossAlgorithm+" Credential="+credentials.AccessKeyID+"/"+scope+",Signature="+signature)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ossTranslateResponse copies x-oss-* response headers to their
|
||||
// x-amz-* equivalents so the SDK can parse them, eg for user
|
||||
// metadata, version IDs and storage class.
|
||||
func ossTranslateResponse(header http.Header) {
|
||||
additions := make(map[string][]string)
|
||||
for k, vs := range header {
|
||||
suffix, ok := strings.CutPrefix(strings.ToLower(k), ossPrefix)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
amzKey := ossAmzPrefix + suffix
|
||||
switch suffix {
|
||||
case "server-side-encryption-key-id":
|
||||
amzKey = "x-amz-server-side-encryption-aws-kms-key-id"
|
||||
case "storage-class":
|
||||
// OSS uses different case sensitive storage class names
|
||||
mapped := make([]string, len(vs))
|
||||
for i, v := range vs {
|
||||
if aws, ok := ossStorageClassFromOSS[v]; ok {
|
||||
mapped[i] = aws
|
||||
} else {
|
||||
mapped[i] = v
|
||||
}
|
||||
}
|
||||
vs = mapped
|
||||
}
|
||||
canonical := textproto.CanonicalMIMEHeaderKey(amzKey)
|
||||
if _, exists := header[canonical]; !exists {
|
||||
additions[canonical] = vs
|
||||
}
|
||||
}
|
||||
for k, vs := range additions {
|
||||
header[k] = vs
|
||||
}
|
||||
}
|
||||
|
||||
// ossTransport is an http.RoundTripper which makes OSS responses
|
||||
// look like S3 responses to the SDK.
|
||||
type ossTransport struct {
|
||||
base http.RoundTripper
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper
|
||||
func (t *ossTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
resp, err := t.base.RoundTrip(req)
|
||||
if resp != nil {
|
||||
ossTranslateResponse(resp.Header)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// ossFixupResponses returns a copy of client whose responses have
|
||||
// their x-oss-* headers copied to the x-amz-* equivalents.
|
||||
func ossFixupResponses(client *http.Client) *http.Client {
|
||||
newClient := *client
|
||||
if newClient.Transport == nil {
|
||||
newClient.Transport = http.DefaultTransport
|
||||
}
|
||||
newClient.Transport = &ossTransport{base: newClient.Transport}
|
||||
return &newClient
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ossV4TestRequest makes the test request used by the signing test
|
||||
// vectors from the official OSS Go SDK:
|
||||
// https://github.com/aliyun/alibabacloud-oss-go-sdk-v2/blob/master/oss/signer/signer_test.go
|
||||
func ossV4TestRequest(t *testing.T, host string) *http.Request {
|
||||
req, err := http.NewRequest("PUT", "http://"+host+"/1234%2B-/123/1.txt", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("x-oss-head1", "value")
|
||||
req.Header.Add("abc", "value")
|
||||
req.Header.Add("ZAbc", "value")
|
||||
req.Header.Add("XYZ", "value")
|
||||
req.Header.Add("content-type", "text/plain")
|
||||
values := url.Values{}
|
||||
values.Add("param1", "value1")
|
||||
values.Add("+param1", "value3")
|
||||
values.Add("|param1", "value4")
|
||||
values.Add("+param2", "")
|
||||
values.Add("|param2", "")
|
||||
values.Add("param2", "")
|
||||
req.URL.RawQuery = values.Encode()
|
||||
return req
|
||||
}
|
||||
|
||||
func TestOSSV4SignHTTP(t *testing.T) {
|
||||
signer := &ossV4Signer{
|
||||
region: "cn-hangzhou",
|
||||
product: "oss",
|
||||
endpointHost: "oss-cn-hangzhou.aliyuncs.com",
|
||||
}
|
||||
req := ossV4TestRequest(t, "bucket.oss-cn-hangzhou.aliyuncs.com")
|
||||
creds := aws.Credentials{AccessKeyID: "ak", SecretAccessKey: "sk"}
|
||||
err := signer.SignHTTP(context.Background(), creds, req, "", "s3", "us-east-1", time.Unix(1702743657, 0))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "OSS4-HMAC-SHA256 Credential=ak/20231216/cn-hangzhou/oss/aliyun_v4_request,Signature=e21d18daa82167720f9b1047ae7e7f1ce7cb77a31e8203a7d5f4624fa0284afe", req.Header.Get("Authorization"))
|
||||
assert.Equal(t, time.Unix(1702743657, 0).UTC().Format("20060102T150405Z"), req.Header.Get("x-oss-date"))
|
||||
assert.Equal(t, "UNSIGNED-PAYLOAD", req.Header.Get("x-oss-content-sha256"))
|
||||
}
|
||||
|
||||
func TestOSSV4SignHTTPToken(t *testing.T) {
|
||||
signer := &ossV4Signer{
|
||||
region: "cn-hangzhou",
|
||||
product: "oss",
|
||||
endpointHost: "oss-cn-hangzhou.aliyuncs.com",
|
||||
}
|
||||
req := ossV4TestRequest(t, "bucket.oss-cn-hangzhou.aliyuncs.com")
|
||||
creds := aws.Credentials{AccessKeyID: "ak", SecretAccessKey: "sk", SessionToken: "token"}
|
||||
err := signer.SignHTTP(context.Background(), creds, req, "", "s3", "us-east-1", time.Unix(1702784856, 0))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "OSS4-HMAC-SHA256 Credential=ak/20231217/cn-hangzhou/oss/aliyun_v4_request,Signature=b94a3f999cf85bcdc00d332fbd3734ba03e48382c36fa4d5af5df817395bd9ea", req.Header.Get("Authorization"))
|
||||
assert.Equal(t, "token", req.Header.Get("x-oss-security-token"))
|
||||
}
|
||||
|
||||
func TestOSSV4SignHTTPCloudBox(t *testing.T) {
|
||||
signer := &ossV4Signer{
|
||||
region: "cb-123",
|
||||
product: "oss-cloudbox",
|
||||
endpointHost: "cb-123.cn-hangzhou.oss-cloudbox.aliyuncs.com",
|
||||
}
|
||||
req := ossV4TestRequest(t, "bucket.cb-123.cn-hangzhou.oss-cloudbox.aliyuncs.com")
|
||||
creds := aws.Credentials{AccessKeyID: "ak", SecretAccessKey: "sk"}
|
||||
err := signer.SignHTTP(context.Background(), creds, req, "", "s3", "us-east-1", time.Unix(1702743657, 0))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "OSS4-HMAC-SHA256 Credential=ak/20231216/cb-123/oss-cloudbox/aliyun_v4_request,Signature=94ce1f12c17d148ea681030275a94449d3357f5b5b21133996eec80af3e08a43", req.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func TestOSSRegionProduct(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
host string
|
||||
region string
|
||||
product string
|
||||
}{
|
||||
{"oss-cn-hangzhou.aliyuncs.com", "cn-hangzhou", "oss"},
|
||||
{"oss-cn-hangzhou-internal.aliyuncs.com", "cn-hangzhou", "oss"},
|
||||
{"oss-cn-jinan-acdr-ut-1-internal.aliyuncs.com", "cn-jinan-acdr-ut-1", "oss"},
|
||||
{"oss-us-east-1.aliyuncs.com", "us-east-1", "oss"},
|
||||
{"oss-accelerate.aliyuncs.com", "", "oss"},
|
||||
{"oss-accelerate-overseas.aliyuncs.com", "", "oss"},
|
||||
{"cb-123.cn-hangzhou.oss-cloudbox.aliyuncs.com", "cb-123", "oss-cloudbox"},
|
||||
{"cb-123.cn-hangzhou.oss-cloudbox-internal.aliyuncs.com", "cb-123", "oss-cloudbox"},
|
||||
{"my.custom.domain.example.com", "", "oss"},
|
||||
{"", "", "oss"},
|
||||
} {
|
||||
region, product := ossRegionProduct(test.host)
|
||||
assert.Equal(t, test.region, region, test.host)
|
||||
assert.Equal(t, test.product, product, test.host)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSSEndpointHost(t *testing.T) {
|
||||
assert.Equal(t, "oss-cn-hangzhou.aliyuncs.com", ossEndpointHost("oss-cn-hangzhou.aliyuncs.com"))
|
||||
assert.Equal(t, "oss-cn-hangzhou.aliyuncs.com", ossEndpointHost("https://oss-cn-hangzhou.aliyuncs.com"))
|
||||
assert.Equal(t, "oss-cn-hangzhou.aliyuncs.com", ossEndpointHost("http://OSS-cn-hangzhou.aliyuncs.com:8080"))
|
||||
assert.Equal(t, "", ossEndpointHost(""))
|
||||
}
|
||||
|
||||
func TestOSSCanonicalURI(t *testing.T) {
|
||||
signer := &ossV4Signer{endpointHost: "oss-cn-hangzhou.aliyuncs.com"}
|
||||
for _, test := range []struct {
|
||||
url string
|
||||
want string
|
||||
hasKey bool
|
||||
}{
|
||||
// virtual hosted style
|
||||
{"https://bucket.oss-cn-hangzhou.aliyuncs.com/", "/bucket/", false},
|
||||
{"https://bucket.oss-cn-hangzhou.aliyuncs.com/key", "/bucket/key", true},
|
||||
{"https://bucket.oss-cn-hangzhou.aliyuncs.com/dir/key", "/bucket/dir/key", true},
|
||||
{"https://bucket.oss-cn-hangzhou.aliyuncs.com", "/bucket/", false},
|
||||
// path style
|
||||
{"https://oss-cn-hangzhou.aliyuncs.com/", "/", false},
|
||||
{"https://oss-cn-hangzhou.aliyuncs.com/bucket", "/bucket", false},
|
||||
{"https://oss-cn-hangzhou.aliyuncs.com/bucket/key", "/bucket/key", true},
|
||||
} {
|
||||
u, err := url.Parse(test.url)
|
||||
require.NoError(t, err)
|
||||
uri := signer.ossCanonicalURI(u)
|
||||
assert.Equal(t, test.want, uri, test.url)
|
||||
hasKey := strings.Contains(strings.Trim(uri, "/"), "/")
|
||||
assert.Equal(t, test.hasKey, hasKey, test.url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSSTranslateRequest(t *testing.T) {
|
||||
req, err := http.NewRequest("PUT", "https://bucket.oss-cn-hangzhou.aliyuncs.com/key", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("X-Amz-Meta-Mtime", "1400000000.000000")
|
||||
req.Header.Set("X-Amz-Copy-Source", "bucket/path/file")
|
||||
req.Header.Set("X-Amz-Acl", "private")
|
||||
req.Header.Set("X-Amz-Storage-Class", "GLACIER")
|
||||
req.Header.Set("X-Amz-Date", "junk")
|
||||
req.Header.Set("X-Amz-Content-Sha256", "junk")
|
||||
req.Header.Set("X-Amz-Security-Token", "junk")
|
||||
req.Header.Set("X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id", "keyid")
|
||||
req.Header.Set("Content-Type", "text/plain")
|
||||
ossTranslateRequest(req, true)
|
||||
assert.Equal(t, "1400000000.000000", req.Header.Get("x-oss-meta-mtime"))
|
||||
assert.Equal(t, "/bucket/path/file", req.Header.Get("x-oss-copy-source"))
|
||||
assert.Equal(t, "private", req.Header.Get("x-oss-object-acl"))
|
||||
assert.Equal(t, "Archive", req.Header.Get("x-oss-storage-class"))
|
||||
assert.Equal(t, "keyid", req.Header.Get("x-oss-server-side-encryption-key-id"))
|
||||
assert.Equal(t, "text/plain", req.Header.Get("Content-Type"))
|
||||
for k := range req.Header {
|
||||
assert.False(t, strings.HasPrefix(strings.ToLower(k), "x-amz-"), k)
|
||||
}
|
||||
|
||||
// a copy source which already has a leading / is left alone
|
||||
req.Header.Set("X-Amz-Copy-Source", "/bucket/path/file")
|
||||
ossTranslateRequest(req, true)
|
||||
assert.Equal(t, "/bucket/path/file", req.Header.Get("x-oss-copy-source"))
|
||||
|
||||
// storage class names are mapped to the native OSS names
|
||||
for _, test := range []struct{ in, want string }{
|
||||
{"STANDARD", "Standard"},
|
||||
{"STANDARD_IA", "IA"},
|
||||
{"GLACIER", "Archive"},
|
||||
{"DEEP_ARCHIVE", "ColdArchive"},
|
||||
{"DEEPCOLDARCHIVE", "DeepColdArchive"},
|
||||
{"Standard", "Standard"},
|
||||
{"IA", "IA"},
|
||||
{"unknown", "unknown"},
|
||||
} {
|
||||
req.Header.Del("x-oss-storage-class")
|
||||
req.Header.Set("X-Amz-Storage-Class", test.in)
|
||||
ossTranslateRequest(req, true)
|
||||
assert.Equal(t, test.want, req.Header.Get("x-oss-storage-class"), test.in)
|
||||
}
|
||||
|
||||
// bucket level requests use x-oss-acl
|
||||
req, err = http.NewRequest("PUT", "https://bucket.oss-cn-hangzhou.aliyuncs.com/", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("X-Amz-Acl", "private")
|
||||
ossTranslateRequest(req, false)
|
||||
assert.Equal(t, "private", req.Header.Get("x-oss-acl"))
|
||||
}
|
||||
|
||||
func TestOSSTranslateResponse(t *testing.T) {
|
||||
header := http.Header{}
|
||||
header.Set("X-Oss-Meta-Mtime", "1400000000.000000")
|
||||
header.Set("X-Oss-Request-Id", "1234")
|
||||
header.Set("X-Oss-Version-Id", "abcd")
|
||||
header.Set("X-Oss-Storage-Class", "Standard")
|
||||
header.Set("X-Oss-Server-Side-Encryption-Key-Id", "keyid")
|
||||
header.Set("Etag", `"abc"`)
|
||||
ossTranslateResponse(header)
|
||||
assert.Equal(t, "1400000000.000000", header.Get("x-amz-meta-mtime"))
|
||||
assert.Equal(t, "1234", header.Get("x-amz-request-id"))
|
||||
assert.Equal(t, "abcd", header.Get("x-amz-version-id"))
|
||||
assert.Equal(t, "STANDARD", header.Get("x-amz-storage-class"))
|
||||
assert.Equal(t, "keyid", header.Get("x-amz-server-side-encryption-aws-kms-key-id"))
|
||||
// the x-oss- headers are left in place
|
||||
assert.Equal(t, "1234", header.Get("x-oss-request-id"))
|
||||
// an existing x-amz- header is not overwritten
|
||||
header = http.Header{}
|
||||
header.Set("X-Oss-Meta-Mtime", "1")
|
||||
header.Set("X-Amz-Meta-Mtime", "2")
|
||||
ossTranslateResponse(header)
|
||||
assert.Equal(t, "2", header.Get("x-amz-meta-mtime"))
|
||||
}
|
||||
@@ -35,3 +35,4 @@ storage_class:
|
||||
bucket_acl: true
|
||||
quirks:
|
||||
use_multipart_etag: false # multipar ETags differ from AWS
|
||||
use_oss_v4_auth: true # some endpoints only accept OSS V4 signatures
|
||||
@@ -30,6 +30,7 @@ type Quirks struct {
|
||||
MightGzip *bool `yaml:"might_gzip,omitempty"`
|
||||
UseMultipartUploads *bool `yaml:"use_multipart_uploads,omitempty"`
|
||||
UseUnsignedPayload *bool `yaml:"use_unsigned_payload,omitempty"`
|
||||
UseOSSV4Auth *bool `yaml:"use_oss_v4_auth,omitempty"`
|
||||
ListVersionsOldestFirst *bool `yaml:"list_versions_oldest_first,omitempty"`
|
||||
UseXID *bool `yaml:"use_x_id,omitempty"`
|
||||
SignAcceptEncoding *bool `yaml:"sign_accept_encoding,omitempty"`
|
||||
|
||||
@@ -393,6 +393,25 @@ If it is set then rclone will use v2 authentication.
|
||||
Use this only if v4 signatures don't work, e.g. pre Jewel/v10 CEPH.`,
|
||||
Default: false,
|
||||
Advanced: true,
|
||||
}, {
|
||||
Name: "use_oss_v4_auth",
|
||||
Help: `If true use Alibaba Cloud OSS native V4 authentication (OSS4-HMAC-SHA256).
|
||||
|
||||
Some Alibaba Cloud OSS endpoints (for example dedicated regions) only
|
||||
accept OSS V4 signatures and reject AWS style signatures with the
|
||||
error "Only support authorization for OSS4-HMAC-SHA256". Alibaba
|
||||
require OSS V4 signatures for accounts created after March 2025 and
|
||||
buckets created after September 2025.
|
||||
|
||||
This is enabled by default for the Alibaba provider.
|
||||
|
||||
The region for the signature is read from the endpoint if possible,
|
||||
otherwise the region option is used.
|
||||
|
||||
Note that presigned requests (rclone link and use_presigned_request)
|
||||
are still signed with AWS style signatures.`,
|
||||
Default: fs.Tristate{},
|
||||
Advanced: true,
|
||||
}, {
|
||||
Name: "use_dual_stack",
|
||||
Help: `If true use AWS S3 dual-stack endpoint (IPv6 support).
|
||||
@@ -1109,6 +1128,7 @@ type Options struct {
|
||||
UploadConcurrency int `config:"upload_concurrency"`
|
||||
ForcePathStyle bool `config:"force_path_style"`
|
||||
V2Auth bool `config:"v2_auth"`
|
||||
UseOSSV4Auth fs.Tristate `config:"use_oss_v4_auth"`
|
||||
UseAccelerateEndpoint bool `config:"use_accelerate_endpoint"`
|
||||
UseARNRegion bool `config:"use_arn_region"`
|
||||
LeavePartsOnError bool `config:"leave_parts_on_error"`
|
||||
@@ -1586,6 +1606,11 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (s3Cli
|
||||
}
|
||||
|
||||
setQuirks(opt, provider)
|
||||
useOSSV4Auth := opt.UseOSSV4Auth.Value && !opt.V2Auth && opt.Region != "other-v2-signature"
|
||||
if useOSSV4Auth {
|
||||
// Make OSS responses look like S3 responses
|
||||
client = ossFixupResponses(client)
|
||||
}
|
||||
awsConfig.RetryMaxAttempts = ci.LowLevelRetries
|
||||
awsConfig.HTTPClient = client
|
||||
|
||||
@@ -1636,6 +1661,12 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (s3Cli
|
||||
s3Opt.HTTPSignerV4 = &v2Signer{opt: opt}
|
||||
})
|
||||
}
|
||||
} else if useOSSV4Auth {
|
||||
signer := newOSSV4Signer(opt)
|
||||
fs.Debugf(nil, "Using OSS V4 auth with signing region %q", signer.region)
|
||||
options = append(options, func(s3Opt *s3.Options) {
|
||||
s3Opt.HTTPSignerV4 = signer
|
||||
})
|
||||
}
|
||||
|
||||
// Fixup the request if needed
|
||||
@@ -1809,6 +1840,7 @@ func setQuirks(opt *Options, provider *Provider) {
|
||||
opt.UploadCutoff = math.MaxInt64
|
||||
}
|
||||
set(&opt.UseUnsignedPayload, true, provider.Quirks.UseUnsignedPayload)
|
||||
set(&opt.UseOSSV4Auth, false, provider.Quirks.UseOSSV4Auth)
|
||||
set(&opt.UseXID, true, provider.Quirks.UseXID)
|
||||
set(&opt.SignAcceptEncoding, true, provider.Quirks.SignAcceptEncoding)
|
||||
set(&opt.ObjectLockSupported, true, provider.Quirks.ObjectLockSupported)
|
||||
|
||||
Reference in new issue
Block a user