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