forked from TrueCloudLab/rclone
Before this change the v2 signer sorted the headers for signing as joined key:value pairs. However this put these two headers in the wrong order. x-amz-copy-source-range: x-amz-copy-source: This changes sorts on the keys before joining the values producing the correct sort order. x-amz-copy-source: x-amz-copy-source-range: This commit also adds some missing query parameters for signing that I spotted in the s3cmd source.
143 lines
3.9 KiB
Go
143 lines
3.9 KiB
Go
// v2 signing
|
|
|
|
package s3
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
)
|
|
|
|
// URL parameters that need to be added to the signature
|
|
var s3ParamsToSign = map[string]struct{}{
|
|
"acl": {},
|
|
"location": {},
|
|
"logging": {},
|
|
"notification": {},
|
|
"partNumber": {},
|
|
"policy": {},
|
|
"requestPayment": {},
|
|
"torrent": {},
|
|
"uploadId": {},
|
|
"uploads": {},
|
|
"versionId": {},
|
|
"versioning": {},
|
|
"versions": {},
|
|
"response-content-type": {},
|
|
"response-content-language": {},
|
|
"response-expires": {},
|
|
"response-cache-control": {},
|
|
"response-content-disposition": {},
|
|
"response-content-encoding": {},
|
|
"lifecycle": {},
|
|
"website": {},
|
|
"delete": {},
|
|
"cors": {},
|
|
"restore": {},
|
|
}
|
|
|
|
// Warn once about empty endpoint
|
|
var warnEmptyEndpointOnce sync.Once
|
|
|
|
// sign signs requests using v2 auth
|
|
//
|
|
// Cobbled together from goamz and aws-sdk-go
|
|
func v2sign(opt *Options, req *http.Request) {
|
|
// Set date
|
|
date := time.Now().UTC().Format(time.RFC1123)
|
|
req.Header.Set("Date", date)
|
|
|
|
// Sort out URI
|
|
uri := req.URL.EscapedPath()
|
|
if uri == "" {
|
|
uri = "/"
|
|
}
|
|
// If not using path style then need to stick the bucket on
|
|
// the start of the requests if doing a bucket based query
|
|
if !opt.ForcePathStyle {
|
|
if opt.Endpoint == "" {
|
|
warnEmptyEndpointOnce.Do(func() {
|
|
fs.Logf(nil, "If using v2 auth with AWS and force_path_style=false, endpoint must be set in the config")
|
|
})
|
|
} else if req.URL.Host != opt.Endpoint {
|
|
// read the bucket off the start of the hostname
|
|
i := strings.IndexRune(req.URL.Host, '.')
|
|
if i >= 0 {
|
|
uri = "/" + req.URL.Host[:i] + uri
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look through headers of interest
|
|
var md5 string
|
|
var contentType string
|
|
var headersToSign [][2]string // slice of key, value pairs
|
|
for k, v := range req.Header {
|
|
k = strings.ToLower(k)
|
|
switch k {
|
|
case "content-md5":
|
|
md5 = v[0]
|
|
case "content-type":
|
|
contentType = v[0]
|
|
default:
|
|
if strings.HasPrefix(k, "x-amz-") {
|
|
vall := strings.Join(v, ",")
|
|
headersToSign = append(headersToSign, [2]string{k, vall})
|
|
}
|
|
}
|
|
}
|
|
// Make headers of interest into canonical string
|
|
var joinedHeadersToSign string
|
|
if len(headersToSign) > 0 {
|
|
// sort by keys
|
|
sort.Slice(headersToSign, func(i, j int) bool {
|
|
return headersToSign[i][0] < headersToSign[j][0]
|
|
})
|
|
// join into key:value\n
|
|
var out strings.Builder
|
|
for _, kv := range headersToSign {
|
|
out.WriteString(kv[0])
|
|
out.WriteRune(':')
|
|
out.WriteString(kv[1])
|
|
out.WriteRune('\n')
|
|
}
|
|
joinedHeadersToSign = out.String()
|
|
}
|
|
|
|
// Look for query parameters which need to be added to the signature
|
|
params := req.URL.Query()
|
|
var queriesToSign []string
|
|
for k, vs := range params {
|
|
if _, ok := s3ParamsToSign[k]; ok {
|
|
for _, v := range vs {
|
|
if v == "" {
|
|
queriesToSign = append(queriesToSign, k)
|
|
} else {
|
|
queriesToSign = append(queriesToSign, k+"="+v)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Add query parameters to URI
|
|
if len(queriesToSign) > 0 {
|
|
sort.StringSlice(queriesToSign).Sort()
|
|
uri += "?" + strings.Join(queriesToSign, "&")
|
|
}
|
|
|
|
// Make signature
|
|
payload := req.Method + "\n" + md5 + "\n" + contentType + "\n" + date + "\n" + joinedHeadersToSign + uri
|
|
hash := hmac.New(sha1.New, []byte(opt.SecretAccessKey))
|
|
_, _ = hash.Write([]byte(payload))
|
|
signature := make([]byte, base64.StdEncoding.EncodedLen(hash.Size()))
|
|
base64.StdEncoding.Encode(signature, hash.Sum(nil))
|
|
|
|
// Set signature in request
|
|
req.Header.Set("Authorization", "AWS "+opt.AccessKeyID+":"+string(signature))
|
|
}
|