You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							378 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							378 lines
						
					
					
						
							14 KiB
						
					
					
				| package policy | |
| 
 | |
| /* | |
|  * MinIO Cloud Storage, (C) 2016, 2017, 2018 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. | |
|  */ | |
| 
 | |
| import ( | |
| 	"bytes" | |
| 	"crypto/hmac" | |
| 	"crypto/sha1" | |
| 	"crypto/sha256" | |
| 	"encoding/base64" | |
| 	"encoding/hex" | |
| 	"fmt" | |
| 	"mime/multipart" | |
| 	"net/http" | |
| 	"net/url" | |
| 	"regexp" | |
| 	"strings" | |
| 	"time" | |
| 	"unicode/utf8" | |
| ) | |
| 
 | |
| const ( | |
| 	iso8601DateFormat = "20060102T150405Z" | |
| 	iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision. | |
| ) | |
| 
 | |
| func newPostPolicyBytesV4WithContentRange(credential, bucketName, objectKey string, expiration time.Time) []byte { | |
| 	t := time.Now().UTC() | |
| 	// Add the expiration date. | |
| 	expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(iso8601TimeFormat)) | |
| 	// Add the bucket condition, only accept buckets equal to the one passed. | |
| 	bucketConditionStr := fmt.Sprintf(`["eq", "$bucket", "%s"]`, bucketName) | |
| 	// Add the key condition, only accept keys equal to the one passed. | |
| 	keyConditionStr := fmt.Sprintf(`["eq", "$key", "%s/upload.txt"]`, objectKey) | |
| 	// Add content length condition, only accept content sizes of a given length. | |
| 	contentLengthCondStr := `["content-length-range", 1024, 1048576]` | |
| 	// Add the algorithm condition, only accept AWS SignV4 Sha256. | |
| 	algorithmConditionStr := `["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"]` | |
| 	// Add the date condition, only accept the current date. | |
| 	dateConditionStr := fmt.Sprintf(`["eq", "$x-amz-date", "%s"]`, t.Format(iso8601DateFormat)) | |
| 	// Add the credential string, only accept the credential passed. | |
| 	credentialConditionStr := fmt.Sprintf(`["eq", "$x-amz-credential", "%s"]`, credential) | |
| 	// Add the meta-uuid string, set to 1234 | |
| 	uuidConditionStr := fmt.Sprintf(`["eq", "$x-amz-meta-uuid", "%s"]`, "1234") | |
| 
 | |
| 	// Combine all conditions into one string. | |
| 	conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s, %s]`, bucketConditionStr, | |
| 		keyConditionStr, contentLengthCondStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr) | |
| 	retStr := "{" | |
| 	retStr = retStr + expirationStr + "," | |
| 	retStr = retStr + conditionStr | |
| 	retStr = retStr + "}" | |
| 
 | |
| 	return []byte(retStr) | |
| } | |
| 
 | |
| // newPostPolicyBytesV4 - creates a bare bones postpolicy string with key and bucket matches. | |
| func newPostPolicyBytesV4(credential, bucketName, objectKey string, expiration time.Time) []byte { | |
| 	t := time.Now().UTC() | |
| 	// Add the expiration date. | |
| 	expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(iso8601TimeFormat)) | |
| 	// Add the bucket condition, only accept buckets equal to the one passed. | |
| 	bucketConditionStr := fmt.Sprintf(`["eq", "$bucket", "%s"]`, bucketName) | |
| 	// Add the key condition, only accept keys equal to the one passed. | |
| 	keyConditionStr := fmt.Sprintf(`["eq", "$key", "%s/upload.txt"]`, objectKey) | |
| 	// Add the algorithm condition, only accept AWS SignV4 Sha256. | |
| 	algorithmConditionStr := `["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"]` | |
| 	// Add the date condition, only accept the current date. | |
| 	dateConditionStr := fmt.Sprintf(`["eq", "$x-amz-date", "%s"]`, t.Format(iso8601DateFormat)) | |
| 	// Add the credential string, only accept the credential passed. | |
| 	credentialConditionStr := fmt.Sprintf(`["eq", "$x-amz-credential", "%s"]`, credential) | |
| 	// Add the meta-uuid string, set to 1234 | |
| 	uuidConditionStr := fmt.Sprintf(`["eq", "$x-amz-meta-uuid", "%s"]`, "1234") | |
| 
 | |
| 	// Combine all conditions into one string. | |
| 	conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s]`, bucketConditionStr, keyConditionStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr) | |
| 	retStr := "{" | |
| 	retStr = retStr + expirationStr + "," | |
| 	retStr = retStr + conditionStr | |
| 	retStr = retStr + "}" | |
| 
 | |
| 	return []byte(retStr) | |
| } | |
| 
 | |
| // newPostPolicyBytesV2 - creates a bare bones postpolicy string with key and bucket matches. | |
| func newPostPolicyBytesV2(bucketName, objectKey string, expiration time.Time) []byte { | |
| 	// Add the expiration date. | |
| 	expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(iso8601TimeFormat)) | |
| 	// Add the bucket condition, only accept buckets equal to the one passed. | |
| 	bucketConditionStr := fmt.Sprintf(`["eq", "$bucket", "%s"]`, bucketName) | |
| 	// Add the key condition, only accept keys equal to the one passed. | |
| 	keyConditionStr := fmt.Sprintf(`["starts-with", "$key", "%s/upload.txt"]`, objectKey) | |
| 
 | |
| 	// Combine all conditions into one string. | |
| 	conditionStr := fmt.Sprintf(`"conditions":[%s, %s]`, bucketConditionStr, keyConditionStr) | |
| 	retStr := "{" | |
| 	retStr = retStr + expirationStr + "," | |
| 	retStr = retStr + conditionStr | |
| 	retStr = retStr + "}" | |
| 
 | |
| 	return []byte(retStr) | |
| } | |
| 
 | |
| // Wrapper for calling TestPostPolicyBucketHandler tests for both Erasure multiple disks and single node setup. | |
|  | |
| // testPostPolicyBucketHandler - Tests validate post policy handler uploading objects. | |
|  | |
| // Wrapper for calling TestPostPolicyBucketHandlerRedirect tests for both Erasure multiple disks and single node setup. | |
|  | |
| // testPostPolicyBucketHandlerRedirect tests POST Object when success_action_redirect is specified | |
|  | |
| // postPresignSignatureV4 - presigned signature for PostPolicy requests. | |
| func postPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, location string) string { | |
| 	// Get signing key. | |
| 	signingkey := getSigningKey(secretAccessKey, t, location) | |
| 	// Calculate signature. | |
| 	signature := getSignature(signingkey, policyBase64) | |
| 	return signature | |
| } | |
| 
 | |
| // copied from auth_signature_v4.go to break import loop | |
| // sumHMAC calculate hmac between two input byte array. | |
| func sumHMAC(key []byte, data []byte) []byte { | |
| 	hash := hmac.New(sha256.New, key) | |
| 	hash.Write(data) | |
| 	return hash.Sum(nil) | |
| } | |
| 
 | |
| // copied from auth_signature_v4.go to break import loop | |
| // getSigningKey hmac seed to calculate final signature. | |
| func getSigningKey(secretKey string, t time.Time, region string) []byte { | |
| 	date := sumHMAC([]byte("AWS4"+secretKey), []byte(t.Format("20060102"))) | |
| 	regionBytes := sumHMAC(date, []byte(region)) | |
| 	service := sumHMAC(regionBytes, []byte("s3")) | |
| 	signingKey := sumHMAC(service, []byte("aws4_request")) | |
| 	return signingKey | |
| } | |
| 
 | |
| // copied from auth_signature_v4.go to break import loop | |
| // getSignature final signature in hexadecimal form. | |
| func getSignature(signingKey []byte, stringToSign string) string { | |
| 	return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) | |
| } | |
| 
 | |
| // copied from auth_signature_v4.go to break import loop | |
| func calculateSignatureV2(stringToSign string, secret string) string { | |
| 	hm := hmac.New(sha1.New, []byte(secret)) | |
| 	hm.Write([]byte(stringToSign)) | |
| 	return base64.StdEncoding.EncodeToString(hm.Sum(nil)) | |
| } | |
| 
 | |
| func newPostRequestV2(endPoint, bucketName, objectName string, accessKey, secretKey string) (*http.Request, error) { | |
| 	// Expire the request five minutes from now. | |
| 	expirationTime := time.Now().UTC().Add(time.Minute * 5) | |
| 	// Create a new post policy. | |
| 	policy := newPostPolicyBytesV2(bucketName, objectName, expirationTime) | |
| 	// Only need the encoding. | |
| 	encodedPolicy := base64.StdEncoding.EncodeToString(policy) | |
| 
 | |
| 	// Presign with V4 signature based on the policy. | |
| 	signature := calculateSignatureV2(encodedPolicy, secretKey) | |
| 
 | |
| 	formData := map[string]string{ | |
| 		"AWSAccessKeyId": accessKey, | |
| 		"bucket":         bucketName, | |
| 		"key":            objectName + "/${filename}", | |
| 		"policy":         encodedPolicy, | |
| 		"signature":      signature, | |
| 	} | |
| 
 | |
| 	// Create the multipart form. | |
| 	var buf bytes.Buffer | |
| 	w := multipart.NewWriter(&buf) | |
| 
 | |
| 	// Set the normal formData | |
| 	for k, v := range formData { | |
| 		w.WriteField(k, v) | |
| 	} | |
| 	// Set the File formData | |
| 	writer, err := w.CreateFormFile("file", "upload.txt") | |
| 	if err != nil { | |
| 		// return nil, err | |
| 		return nil, err | |
| 	} | |
| 	writer.Write([]byte("hello world")) | |
| 	// Close before creating the new request. | |
| 	w.Close() | |
| 
 | |
| 	// Set the body equal to the created policy. | |
| 	reader := bytes.NewReader(buf.Bytes()) | |
| 
 | |
| 	req, err := http.NewRequest(http.MethodPost, makeTestTargetURL(endPoint, bucketName, "", nil), reader) | |
| 	if err != nil { | |
| 		return nil, err | |
| 	} | |
| 
 | |
| 	// Set form content-type. | |
| 	req.Header.Set("Content-Type", w.FormDataContentType()) | |
| 	return req, nil | |
| } | |
| 
 | |
| func buildGenericPolicy(t time.Time, accessKey, region, bucketName, objectName string, contentLengthRange bool) []byte { | |
| 	// Expire the request five minutes from now. | |
| 	expirationTime := t.Add(time.Minute * 5) | |
| 
 | |
| 	credStr := getCredentialString(accessKey, region, t) | |
| 	// Create a new post policy. | |
| 	policy := newPostPolicyBytesV4(credStr, bucketName, objectName, expirationTime) | |
| 	if contentLengthRange { | |
| 		policy = newPostPolicyBytesV4WithContentRange(credStr, bucketName, objectName, expirationTime) | |
| 	} | |
| 	return policy | |
| } | |
| 
 | |
| func newPostRequestV4Generic(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string, region string, | |
| 	t time.Time, policy []byte, addFormData map[string]string, corruptedB64 bool, corruptedMultipart bool) (*http.Request, error) { | |
| 	// Get the user credential. | |
| 	credStr := getCredentialString(accessKey, region, t) | |
| 
 | |
| 	// Only need the encoding. | |
| 	encodedPolicy := base64.StdEncoding.EncodeToString(policy) | |
| 
 | |
| 	if corruptedB64 { | |
| 		encodedPolicy = "%!~&" + encodedPolicy | |
| 	} | |
| 
 | |
| 	// Presign with V4 signature based on the policy. | |
| 	signature := postPresignSignatureV4(encodedPolicy, t, secretKey, region) | |
| 
 | |
| 	formData := map[string]string{ | |
| 		"bucket":           bucketName, | |
| 		"key":              objectName + "/${filename}", | |
| 		"x-amz-credential": credStr, | |
| 		"policy":           encodedPolicy, | |
| 		"x-amz-signature":  signature, | |
| 		"x-amz-date":       t.Format(iso8601DateFormat), | |
| 		"x-amz-algorithm":  "AWS4-HMAC-SHA256", | |
| 		"x-amz-meta-uuid":  "1234", | |
| 		"Content-Encoding": "gzip", | |
| 	} | |
| 
 | |
| 	// Add form data | |
| 	for k, v := range addFormData { | |
| 		formData[k] = v | |
| 	} | |
| 
 | |
| 	// Create the multipart form. | |
| 	var buf bytes.Buffer | |
| 	w := multipart.NewWriter(&buf) | |
| 
 | |
| 	// Set the normal formData | |
| 	for k, v := range formData { | |
| 		w.WriteField(k, v) | |
| 	} | |
| 	// Set the File formData but don't if we want send an incomplete multipart request | |
| 	if !corruptedMultipart { | |
| 		writer, err := w.CreateFormFile("file", "upload.txt") | |
| 		if err != nil { | |
| 			// return nil, err | |
| 			return nil, err | |
| 		} | |
| 		writer.Write(objData) | |
| 		// Close before creating the new request. | |
| 		w.Close() | |
| 	} | |
| 
 | |
| 	// Set the body equal to the created policy. | |
| 	reader := bytes.NewReader(buf.Bytes()) | |
| 
 | |
| 	req, err := http.NewRequest(http.MethodPost, makeTestTargetURL(endPoint, bucketName, "", nil), reader) | |
| 	if err != nil { | |
| 		return nil, err | |
| 	} | |
| 
 | |
| 	// Set form content-type. | |
| 	req.Header.Set("Content-Type", w.FormDataContentType()) | |
| 	return req, nil | |
| } | |
| 
 | |
| func newPostRequestV4WithContentLength(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) { | |
| 	t := time.Now().UTC() | |
| 	region := "us-east-1" | |
| 	policy := buildGenericPolicy(t, accessKey, region, bucketName, objectName, true) | |
| 	return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, region, t, policy, nil, false, false) | |
| } | |
| 
 | |
| func newPostRequestV4(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) { | |
| 	t := time.Now().UTC() | |
| 	region := "us-east-1" | |
| 	policy := buildGenericPolicy(t, accessKey, region, bucketName, objectName, false) | |
| 	return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, region, t, policy, nil, false, false) | |
| } | |
| 
 | |
| // construct URL for http requests for bucket operations. | |
| func makeTestTargetURL(endPoint, bucketName, objectName string, queryValues url.Values) string { | |
| 	urlStr := endPoint + "/" | |
| 	if bucketName != "" { | |
| 		urlStr = urlStr + bucketName + "/" | |
| 	} | |
| 	if objectName != "" { | |
| 		urlStr = urlStr + EncodePath(objectName) | |
| 	} | |
| 	if len(queryValues) > 0 { | |
| 		urlStr = urlStr + "?" + queryValues.Encode() | |
| 	} | |
| 	return urlStr | |
| } | |
| 
 | |
| // if object matches reserved string, no need to encode them | |
| var reservedObjectNames = regexp.MustCompile("^[a-zA-Z0-9-_.~/]+$") | |
| 
 | |
| // EncodePath encode the strings from UTF-8 byte representations to HTML hex escape sequences | |
| // | |
| // This is necessary since regular url.Parse() and url.Encode() functions do not support UTF-8 | |
| // non english characters cannot be parsed due to the nature in which url.Encode() is written | |
| // | |
| // This function on the other hand is a direct replacement for url.Encode() technique to support | |
| // pretty much every UTF-8 character. | |
| func EncodePath(pathName string) string { | |
| 	if reservedObjectNames.MatchString(pathName) { | |
| 		return pathName | |
| 	} | |
| 	var encodedPathname string | |
| 	for _, s := range pathName { | |
| 		if 'A' <= s && s <= 'Z' || 'a' <= s && s <= 'z' || '0' <= s && s <= '9' { // §2.3 Unreserved characters (mark) | |
| 			encodedPathname = encodedPathname + string(s) | |
| 			continue | |
| 		} | |
| 		switch s { | |
| 		case '-', '_', '.', '~', '/': // §2.3 Unreserved characters (mark) | |
| 			encodedPathname = encodedPathname + string(s) | |
| 			continue | |
| 		default: | |
| 			len := utf8.RuneLen(s) | |
| 			if len < 0 { | |
| 				// if utf8 cannot convert return the same string as is | |
| 				return pathName | |
| 			} | |
| 			u := make([]byte, len) | |
| 			utf8.EncodeRune(u, s) | |
| 			for _, r := range u { | |
| 				hex := hex.EncodeToString([]byte{r}) | |
| 				encodedPathname = encodedPathname + "%" + strings.ToUpper(hex) | |
| 			} | |
| 		} | |
| 	} | |
| 	return encodedPathname | |
| } | |
| 
 | |
| // getCredentialString generate a credential string. | |
| func getCredentialString(accessKeyID, location string, t time.Time) string { | |
| 	return accessKeyID + "/" + getScope(t, location) | |
| } | |
| 
 | |
| // getScope generate a string of a specific date, an AWS region, and a service. | |
| func getScope(t time.Time, region string) string { | |
| 	scope := strings.Join([]string{ | |
| 		t.Format("20060102"), | |
| 		region, | |
| 		string("s3"), | |
| 		"aws4_request", | |
| 	}, "/") | |
| 	return scope | |
| }
 |