parent
9fb1c89f81
commit
5885ffc8ae
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,56 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"bytes" |
||||
"net/url" |
||||
"sort" |
||||
"strings" |
||||
) |
||||
|
||||
// Replaces any occurring '/' in string, into its encoded representation.
|
||||
func percentEncodeSlash(s string) string { |
||||
return strings.Replace(s, "/", "%2F", -1) |
||||
} |
||||
|
||||
// queryEncode - encodes query values in their URL encoded form. In
|
||||
// addition to the percent encoding performed by getURLEncodedName() used
|
||||
// here, it also percent encodes '/' (forward slash)
|
||||
func queryEncode(v url.Values) string { |
||||
if v == nil { |
||||
return "" |
||||
} |
||||
var buf bytes.Buffer |
||||
keys := make([]string, 0, len(v)) |
||||
for k := range v { |
||||
keys = append(keys, k) |
||||
} |
||||
sort.Strings(keys) |
||||
for _, k := range keys { |
||||
vs := v[k] |
||||
prefix := percentEncodeSlash(getURLEncodedName(k)) + "=" |
||||
for _, v := range vs { |
||||
if buf.Len() > 0 { |
||||
buf.WriteByte('&') |
||||
} |
||||
buf.WriteString(prefix) |
||||
buf.WriteString(percentEncodeSlash(getURLEncodedName(v))) |
||||
} |
||||
} |
||||
return buf.String() |
||||
} |
@ -0,0 +1,303 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/hmac" |
||||
"crypto/sha1" |
||||
"encoding/base64" |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
"sort" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
// Signature and API related constants.
|
||||
const ( |
||||
signV2Algorithm = "AWS" |
||||
) |
||||
|
||||
// TODO add post policy signature.
|
||||
|
||||
// doesPresignV2SignatureMatch - Verify query headers with presigned signature
|
||||
// - http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
|
||||
// returns ErrNone if matches. S3 errors otherwise.
|
||||
func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode { |
||||
// Access credentials.
|
||||
cred := serverConfig.GetCredential() |
||||
|
||||
// Copy request
|
||||
req := *r |
||||
|
||||
// Validate if we do have query params.
|
||||
if req.URL.Query().Encode() == "" { |
||||
return ErrInvalidQueryParams |
||||
} |
||||
|
||||
// Validate if access key id same.
|
||||
if req.URL.Query().Get("AWSAccessKeyId") != cred.AccessKeyID { |
||||
return ErrInvalidAccessKeyID |
||||
} |
||||
|
||||
// Parse expires param into its native form.
|
||||
expired, err := strconv.ParseInt(req.URL.Query().Get("Expires"), 10, 64) |
||||
if err != nil { |
||||
errorIf(err, "Unable to parse expires query param") |
||||
return ErrMalformedExpires |
||||
} |
||||
|
||||
// Validate if the request has already expired.
|
||||
if expired < time.Now().UTC().Unix() { |
||||
return ErrExpiredPresignRequest |
||||
} |
||||
|
||||
// Get presigned string to sign.
|
||||
stringToSign := preStringifyHTTPReq(req) |
||||
hm := hmac.New(sha1.New, []byte(cred.SecretAccessKey)) |
||||
hm.Write([]byte(stringToSign)) |
||||
|
||||
// Calculate signature and validate.
|
||||
signature := base64.StdEncoding.EncodeToString(hm.Sum(nil)) |
||||
if req.URL.Query().Get("Signature") != signature { |
||||
return ErrSignatureDoesNotMatch |
||||
} |
||||
|
||||
// Success.
|
||||
return ErrNone |
||||
} |
||||
|
||||
// Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature;
|
||||
// Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) );
|
||||
//
|
||||
// StringToSign = HTTP-Verb + "\n" +
|
||||
// Content-Md5 + "\n" +
|
||||
// Content-Type + "\n" +
|
||||
// Date + "\n" +
|
||||
// CanonicalizedProtocolHeaders +
|
||||
// CanonicalizedResource;
|
||||
//
|
||||
// CanonicalizedResource = [ "/" + Bucket ] +
|
||||
// <HTTP-Request-URI, from the protocol name up to the query string> +
|
||||
// [ subresource, if present. For example "?acl", "?location", "?logging", or "?torrent"];
|
||||
//
|
||||
// CanonicalizedProtocolHeaders = <described below>
|
||||
|
||||
// doesSignV2Match - Verify authorization header with calculated header in accordance with
|
||||
// - http://docs.aws.amazon.com/AmazonS3/latest/dev/auth-request-sig-v2.html
|
||||
// returns true if matches, false otherwise. if error is not nil then it is always false
|
||||
func doesSignV2Match(r *http.Request) APIErrorCode { |
||||
// Access credentials.
|
||||
cred := serverConfig.GetCredential() |
||||
|
||||
// Copy request.
|
||||
req := *r |
||||
|
||||
// Save authorization header.
|
||||
v2Auth := req.Header.Get("Authorization") |
||||
if v2Auth == "" { |
||||
return ErrAuthHeaderEmpty |
||||
} |
||||
|
||||
// Add date if not present.
|
||||
if date := req.Header.Get("Date"); date == "" { |
||||
if date = req.Header.Get("X-Amz-Date"); date == "" { |
||||
return ErrMissingDateHeader |
||||
} |
||||
} |
||||
|
||||
// Calculate HMAC for secretAccessKey.
|
||||
stringToSign := stringifyHTTPReq(req) |
||||
hm := hmac.New(sha1.New, []byte(cred.SecretAccessKey)) |
||||
hm.Write([]byte(stringToSign)) |
||||
|
||||
// Prepare auth header.
|
||||
authHeader := new(bytes.Buffer) |
||||
authHeader.WriteString(fmt.Sprintf("%s %s:", signV2Algorithm, cred.AccessKeyID)) |
||||
encoder := base64.NewEncoder(base64.StdEncoding, authHeader) |
||||
encoder.Write(hm.Sum(nil)) |
||||
encoder.Close() |
||||
|
||||
// Verify if signature match.
|
||||
if authHeader.String() != v2Auth { |
||||
return ErrSignatureDoesNotMatch |
||||
} |
||||
|
||||
return ErrNone |
||||
} |
||||
|
||||
// From the Amazon docs:
|
||||
//
|
||||
// StringToSign = HTTP-Verb + "\n" +
|
||||
// Content-Md5 + "\n" +
|
||||
// Content-Type + "\n" +
|
||||
// Expires + "\n" +
|
||||
// CanonicalizedProtocolHeaders +
|
||||
// CanonicalizedResource;
|
||||
func preStringifyHTTPReq(req http.Request) string { |
||||
buf := new(bytes.Buffer) |
||||
// Write standard headers.
|
||||
writePreSignV2Headers(buf, req) |
||||
// Write canonicalized protocol headers if any.
|
||||
writeCanonicalizedHeaders(buf, req) |
||||
// Write canonicalized Query resources if any.
|
||||
isPreSign := true |
||||
writeCanonicalizedResource(buf, req, isPreSign) |
||||
return buf.String() |
||||
} |
||||
|
||||
// writePreSignV2Headers - write preSign v2 required headers.
|
||||
func writePreSignV2Headers(buf *bytes.Buffer, req http.Request) { |
||||
buf.WriteString(req.Method + "\n") |
||||
buf.WriteString(req.Header.Get("Content-Md5") + "\n") |
||||
buf.WriteString(req.Header.Get("Content-Type") + "\n") |
||||
buf.WriteString(req.Header.Get("Expires") + "\n") |
||||
} |
||||
|
||||
// From the Amazon docs:
|
||||
//
|
||||
// StringToSign = HTTP-Verb + "\n" +
|
||||
// Content-Md5 + "\n" +
|
||||
// Content-Type + "\n" +
|
||||
// Date + "\n" +
|
||||
// CanonicalizedProtocolHeaders +
|
||||
// CanonicalizedResource;
|
||||
func stringifyHTTPReq(req http.Request) string { |
||||
buf := new(bytes.Buffer) |
||||
// Write standard headers.
|
||||
writeSignV2Headers(buf, req) |
||||
// Write canonicalized protocol headers if any.
|
||||
writeCanonicalizedHeaders(buf, req) |
||||
// Write canonicalized Query resources if any.
|
||||
isPreSign := false |
||||
writeCanonicalizedResource(buf, req, isPreSign) |
||||
return buf.String() |
||||
} |
||||
|
||||
// writeSignV2Headers - write signV2 required headers.
|
||||
func writeSignV2Headers(buf *bytes.Buffer, req http.Request) { |
||||
buf.WriteString(req.Method + "\n") |
||||
buf.WriteString(req.Header.Get("Content-Md5") + "\n") |
||||
buf.WriteString(req.Header.Get("Content-Type") + "\n") |
||||
buf.WriteString(req.Header.Get("Date") + "\n") |
||||
} |
||||
|
||||
// writeCanonicalizedHeaders - write canonicalized headers.
|
||||
func writeCanonicalizedHeaders(buf *bytes.Buffer, req http.Request) { |
||||
var protoHeaders []string |
||||
vals := make(map[string][]string) |
||||
for k, vv := range req.Header { |
||||
// All the AMZ headers should be lowercase
|
||||
lk := strings.ToLower(k) |
||||
if strings.HasPrefix(lk, "x-amz") { |
||||
protoHeaders = append(protoHeaders, lk) |
||||
vals[lk] = vv |
||||
} |
||||
} |
||||
sort.Strings(protoHeaders) |
||||
for _, k := range protoHeaders { |
||||
buf.WriteString(k) |
||||
buf.WriteByte(':') |
||||
for idx, v := range vals[k] { |
||||
if idx > 0 { |
||||
buf.WriteByte(',') |
||||
} |
||||
if strings.Contains(v, "\n") { |
||||
// TODO: "Unfold" long headers that
|
||||
// span multiple lines (as allowed by
|
||||
// RFC 2616, section 4.2) by replacing
|
||||
// the folding white-space (including
|
||||
// new-line) by a single space.
|
||||
buf.WriteString(v) |
||||
} else { |
||||
buf.WriteString(v) |
||||
} |
||||
} |
||||
buf.WriteByte('\n') |
||||
} |
||||
} |
||||
|
||||
// The following list is already sorted and should always be, otherwise we could
|
||||
// have signature-related issues
|
||||
var resourceList = []string{ |
||||
"acl", |
||||
"delete", |
||||
"location", |
||||
"logging", |
||||
"notification", |
||||
"partNumber", |
||||
"policy", |
||||
"requestPayment", |
||||
"torrent", |
||||
"uploadId", |
||||
"uploads", |
||||
"versionId", |
||||
"versioning", |
||||
"versions", |
||||
"website", |
||||
} |
||||
|
||||
// From the Amazon docs:
|
||||
//
|
||||
// CanonicalizedResource = [ "/" + Bucket ] +
|
||||
// <HTTP-Request-URI, from the protocol name up to the query string> +
|
||||
// [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"];
|
||||
func writeCanonicalizedResource(buf *bytes.Buffer, req http.Request, isPreSign bool) { |
||||
// Save request URL.
|
||||
requestURL := req.URL |
||||
// Get encoded URL path.
|
||||
path := getURLEncodedName(requestURL.Path) |
||||
if isPreSign { |
||||
// Get encoded URL path.
|
||||
if len(requestURL.Query()) > 0 { |
||||
// Keep the usual queries unescaped for string to sign.
|
||||
query, _ := url.QueryUnescape(queryEncode(requestURL.Query())) |
||||
path = path + "?" + query |
||||
} |
||||
buf.WriteString(path) |
||||
return |
||||
} |
||||
buf.WriteString(path) |
||||
if requestURL.RawQuery != "" { |
||||
var n int |
||||
vals, _ := url.ParseQuery(requestURL.RawQuery) |
||||
// Verify if any sub resource queries are present, if yes
|
||||
// canonicallize them.
|
||||
for _, resource := range resourceList { |
||||
if vv, ok := vals[resource]; ok && len(vv) > 0 { |
||||
n++ |
||||
// First element
|
||||
switch n { |
||||
case 1: |
||||
buf.WriteByte('?') |
||||
// The rest
|
||||
default: |
||||
buf.WriteByte('&') |
||||
} |
||||
buf.WriteString(resource) |
||||
// Request parameters
|
||||
if len(vv[0]) > 0 { |
||||
buf.WriteByte('=') |
||||
buf.WriteString(strings.Replace(url.QueryEscape(vv[0]), "+", "%20", -1)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,159 @@ |
||||
package cmd |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
"sort" |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
// Tests for 'func TestResourceListSorting(t *testing.T)'.
|
||||
func TestResourceListSorting(t *testing.T) { |
||||
sortedResourceList := make([]string, len(resourceList)) |
||||
copy(sortedResourceList, resourceList) |
||||
sort.Strings(sortedResourceList) |
||||
for i := 0; i < len(resourceList); i++ { |
||||
if resourceList[i] != sortedResourceList[i] { |
||||
t.Errorf("Expected resourceList[%d] = \"%s\", resourceList is not correctly sorted.", i, sortedResourceList[i]) |
||||
break |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Tests validate the query encoding.
|
||||
func TestQueryEncode(t *testing.T) { |
||||
testCases := []struct { |
||||
// Input.
|
||||
input url.Values |
||||
// Expected result.
|
||||
result string |
||||
}{ |
||||
// % should be encoded as %25
|
||||
{url.Values{ |
||||
"key": []string{"thisisthe%url"}, |
||||
}, "key=thisisthe%25url"}, |
||||
// UTF-8 encoding.
|
||||
{url.Values{ |
||||
"key": []string{"本語"}, |
||||
}, "key=%E6%9C%AC%E8%AA%9E"}, |
||||
// UTF-8 encoding with ASCII.
|
||||
{url.Values{ |
||||
"key": []string{"本語.1"}, |
||||
}, "key=%E6%9C%AC%E8%AA%9E.1"}, |
||||
// Unusual ASCII characters.
|
||||
{url.Values{ |
||||
"key": []string{">123"}, |
||||
}, "key=%3E123"}, |
||||
// Fragment path characters.
|
||||
{url.Values{ |
||||
"key": []string{"myurl#link"}, |
||||
}, "key=myurl%23link"}, |
||||
// Space should be set to %20 not '+'.
|
||||
{url.Values{ |
||||
"key": []string{"space in url"}, |
||||
}, "key=space%20in%20url"}, |
||||
// '+' shouldn't be treated as space.
|
||||
{url.Values{ |
||||
"key": []string{"url+path"}, |
||||
}, "key=url%2Bpath"}, |
||||
// '/' shouldn't be treated as '/' should be percent coded.
|
||||
{url.Values{ |
||||
"key": []string{"url/+path"}, |
||||
}, "key=url%2F%2Bpath"}, |
||||
// Values is empty and empty string.
|
||||
{nil, ""}, |
||||
} |
||||
|
||||
// Tests generated values from url encoded name.
|
||||
for i, testCase := range testCases { |
||||
result := queryEncode(testCase.input) |
||||
if testCase.result != result { |
||||
t.Errorf("Test %d: Expected queryEncoded result to be \"%s\", but found it to be \"%s\" instead", i+1, testCase.result, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestDoesPresignedV2SignatureMatch(t *testing.T) { |
||||
root, err := newTestConfig("us-east-1") |
||||
if err != nil { |
||||
t.Fatal("Unable to initialize test config.") |
||||
} |
||||
defer removeAll(root) |
||||
|
||||
now := time.Now().UTC() |
||||
|
||||
testCases := []struct { |
||||
queryParams map[string]string |
||||
headers map[string]string |
||||
expected APIErrorCode |
||||
}{ |
||||
// (0) Should error without a set URL query.
|
||||
{ |
||||
expected: ErrInvalidQueryParams, |
||||
}, |
||||
// (1) Should error on an invalid access key.
|
||||
{ |
||||
queryParams: map[string]string{ |
||||
"Expires": "60", |
||||
"Signature": "badsignature", |
||||
"AWSAccessKeyId": "Z7IXGOO6BZ0REAN1Q26I", |
||||
}, |
||||
expected: ErrInvalidAccessKeyID, |
||||
}, |
||||
// (2) Should error with malformed expires.
|
||||
{ |
||||
queryParams: map[string]string{ |
||||
"Expires": "60s", |
||||
"Signature": "badsignature", |
||||
"AWSAccessKeyId": serverConfig.GetCredential().AccessKeyID, |
||||
}, |
||||
expected: ErrMalformedExpires, |
||||
}, |
||||
// (3) Should give an expired request if it has expired.
|
||||
{ |
||||
queryParams: map[string]string{ |
||||
"Expires": "60", |
||||
"Signature": "badsignature", |
||||
"AWSAccessKeyId": serverConfig.GetCredential().AccessKeyID, |
||||
}, |
||||
expected: ErrExpiredPresignRequest, |
||||
}, |
||||
// (4) Should error when the signature does not match.
|
||||
{ |
||||
queryParams: map[string]string{ |
||||
"Expires": fmt.Sprintf("%d", now.Unix()+60), |
||||
"Signature": "badsignature", |
||||
"AWSAccessKeyId": serverConfig.GetCredential().AccessKeyID, |
||||
}, |
||||
expected: ErrSignatureDoesNotMatch, |
||||
}, |
||||
} |
||||
|
||||
// Run each test case individually.
|
||||
for i, testCase := range testCases { |
||||
// Turn the map[string]string into map[string][]string, because Go.
|
||||
query := url.Values{} |
||||
for key, value := range testCase.queryParams { |
||||
query.Set(key, value) |
||||
} |
||||
|
||||
// Create a request to use.
|
||||
req, e := http.NewRequest(http.MethodGet, "http://host/a/b?"+query.Encode(), nil) |
||||
if e != nil { |
||||
t.Errorf("(%d) failed to create http.Request, got %v", i, e) |
||||
} |
||||
|
||||
// Do the same for the headers.
|
||||
for key, value := range testCase.headers { |
||||
req.Header.Set(key, value) |
||||
} |
||||
|
||||
// Check if it matches!
|
||||
err := doesPresignV2SignatureMatch(req) |
||||
if err != testCase.expected { |
||||
t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(err)) |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue