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
							 | 
						|
								}
							 |