diff --git a/cmd/api-errors.go b/cmd/api-errors.go index 6d84d29c3..e6d3be692 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -629,6 +629,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) { apiErr = ErrContentSHA256Mismatch case ObjectTooLarge: apiErr = ErrEntityTooLarge + case ObjectTooSmall: + apiErr = ErrEntityTooSmall default: apiErr = ErrInternalError } diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index 0f834a4ba..80b515cb0 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -470,14 +470,22 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h return } - // Use limitReader to ensure that object size is within expected range. + // Use rangeReader to ensure that object size is within expected range. lengthRange := postPolicyForm.Conditions.ContentLengthRange if lengthRange.Valid { // If policy restricted the size of the object. - fileBody = limitReader(fileBody, int64(lengthRange.Min), int64(lengthRange.Max)) + fileBody = &rangeReader{ + Reader: fileBody, + Min: lengthRange.Min, + Max: lengthRange.Max, + } } else { // Default values of min/max size of the object. - fileBody = limitReader(fileBody, 0, maxObjectSize) + fileBody = &rangeReader{ + Reader: fileBody, + Min: 0, + Max: maxObjectSize, + } } // Save metadata. diff --git a/cmd/object-utils.go b/cmd/object-utils.go index f0942ed6b..d6b68df41 100644 --- a/cmd/object-utils.go +++ b/cmd/object-utils.go @@ -163,27 +163,25 @@ func (d byBucketName) Len() int { return len(d) } func (d byBucketName) Swap(i, j int) { d[i], d[j] = d[j], d[i] } func (d byBucketName) Less(i, j int) bool { return d[i].Name < d[j].Name } -// Copied from io.LimitReader() -// limitReader returns a Reader that reads from r -// but returns error after n bytes. -// The underlying implementation is a *LimitedReader. -type limitedReader struct { - R io.Reader // underlying reader - M int64 // min bytes remaining - N int64 // max bytes remaining +// rangeReader returns a Reader that reads from r +// but returns error after Max bytes read as errDataTooLarge. +// but returns error if reader exits before reading Min bytes +// errDataTooSmall. +type rangeReader struct { + Reader io.Reader // underlying reader + Min int64 // min bytes remaining + Max int64 // max bytes remaining } -func limitReader(r io.Reader, m, n int64) io.Reader { return &limitedReader{r, m, n} } - -func (l *limitedReader) Read(p []byte) (n int, err error) { - n, err = l.R.Read(p) - l.N -= int64(n) - l.M -= int64(n) - if l.N < 0 { +func (l *rangeReader) Read(p []byte) (n int, err error) { + n, err = l.Reader.Read(p) + l.Max -= int64(n) + l.Min -= int64(n) + if l.Max < 0 { // If more data is available than what is expected we return error. return 0, errDataTooLarge } - if err == io.EOF && l.M > 0 { + if err == io.EOF && l.Min > 0 { return 0, errDataTooSmall } return diff --git a/cmd/object-utils_test.go b/cmd/object-utils_test.go index 7f8c7998d..310b94b9e 100644 --- a/cmd/object-utils_test.go +++ b/cmd/object-utils_test.go @@ -116,8 +116,8 @@ func TestIsValidObjectName(t *testing.T) { } } -// Tests limitReader -func TestLimitReader(t *testing.T) { +// Tests rangeReader. +func TestRangeReader(t *testing.T) { testCases := []struct { data string minLen int64 @@ -126,15 +126,19 @@ func TestLimitReader(t *testing.T) { }{ {"1234567890", 0, 15, nil}, {"1234567890", 0, 10, nil}, - {"1234567890", 0, 5, errDataTooLarge}, - {"123", 5, 10, errDataTooSmall}, + {"1234567890", 0, 5, toObjectErr(errDataTooLarge, "test", "test")}, + {"123", 5, 10, toObjectErr(errDataTooSmall, "test", "test")}, {"123", 2, 10, nil}, } for i, test := range testCases { r := strings.NewReader(test.data) - _, err := ioutil.ReadAll(limitReader(r, test.minLen, test.maxLen)) - if err != test.err { + _, err := ioutil.ReadAll(&rangeReader{ + Reader: r, + Min: test.minLen, + Max: test.maxLen, + }) + if toObjectErr(err, "test", "test") != test.err { t.Fatalf("test %d failed: expected %v, got %v", i+1, test.err, err) } } diff --git a/cmd/post-policy_test.go b/cmd/post-policy_test.go index e7b8c8717..281ca1c37 100644 --- a/cmd/post-policy_test.go +++ b/cmd/post-policy_test.go @@ -33,6 +33,34 @@ const ( iso8601DateFormat = "20060102T150405Z" ) +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(expirationDateFormat)) + // 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"]`, 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) + + // Combine all conditions into one string. + conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s]`, bucketConditionStr, + keyConditionStr, contentLengthCondStr, algorithmConditionStr, dateConditionStr, credentialConditionStr) + 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() @@ -206,6 +234,59 @@ func testPostPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandle t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code) } } + + testCases2 := []struct { + objectName string + data []byte + expectedRespStatus int + accessKey string + secretKey string + malformedBody bool + }{ + // Success case. + { + objectName: "test", + data: bytes.Repeat([]byte("a"), 1025), + expectedRespStatus: http.StatusNoContent, + accessKey: credentials.AccessKeyID, + secretKey: credentials.SecretAccessKey, + malformedBody: false, + }, + // Failed with entity too small. + { + objectName: "test", + data: bytes.Repeat([]byte("a"), 1023), + expectedRespStatus: http.StatusBadRequest, + accessKey: credentials.AccessKeyID, + secretKey: credentials.SecretAccessKey, + malformedBody: false, + }, + // Failed with entity too large. + { + objectName: "test", + data: bytes.Repeat([]byte("a"), 1024*1024+1), + expectedRespStatus: http.StatusBadRequest, + accessKey: credentials.AccessKeyID, + secretKey: credentials.SecretAccessKey, + malformedBody: false, + }, + } + + for i, testCase := range testCases2 { + // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler. + rec := httptest.NewRecorder() + req, perr := newPostRequestV4WithContentLength("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, testCase.secretKey) + if perr != nil { + t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: %v", i+1, instanceType, perr) + } + // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler. + // Call the ServeHTTP to execute the handler. + apiRouter.ServeHTTP(rec, req) + if rec.Code != testCase.expectedRespStatus { + t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code) + } + } + } // postPresignSignatureV4 - presigned signature for PostPolicy requests. @@ -267,7 +348,7 @@ func newPostRequestV2(endPoint, bucketName, objectName string, accessKey, secret return req, nil } -func newPostRequestV4(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) { +func newPostRequestV4Generic(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string, contentLengthRange bool) (*http.Request, error) { // Keep time. t := time.Now().UTC() // Expire the request five minutes from now. @@ -276,6 +357,9 @@ func newPostRequestV4(endPoint, bucketName, objectName string, objData []byte, a credStr := getCredential(accessKey, serverConfig.GetRegion(), t) // Create a new post policy. policy := newPostPolicyBytesV4(credStr, bucketName, objectName, expirationTime) + if contentLengthRange { + policy = newPostPolicyBytesV4WithContentRange(credStr, bucketName, objectName, expirationTime) + } // Only need the encoding. encodedPolicy := base64.StdEncoding.EncodeToString(policy) @@ -322,3 +406,11 @@ func newPostRequestV4(endPoint, bucketName, objectName string, objData []byte, a req.Header.Set("Content-Type", w.FormDataContentType()) return req, nil } + +func newPostRequestV4WithContentLength(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) { + return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, true) +} + +func newPostRequestV4(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) { + return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, false) +} diff --git a/cmd/postpolicyform.go b/cmd/postpolicyform.go index fe669b18b..ee68a24b4 100644 --- a/cmd/postpolicyform.go +++ b/cmd/postpolicyform.go @@ -34,10 +34,14 @@ func toString(val interface{}) string { } // toInteger _ Safely convert interface to integer without causing panic. -func toInteger(val interface{}) int { +func toInteger(val interface{}) int64 { switch v := val.(type) { - case int: + case float64: + return int64(v) + case int64: return v + case int: + return int64(v) } return 0 } @@ -53,8 +57,8 @@ func isString(val interface{}) bool { // ContentLengthRange - policy content-length-range field. type contentLengthRange struct { - Min int - Max int + Min int64 + Max int64 Valid bool // If content-length-range was part of policy } @@ -136,7 +140,11 @@ func parsePostPolicyForm(policy string) (PostPolicyForm, error) { Value: value, } case "content-length-range": - parsedPolicy.Conditions.ContentLengthRange = contentLengthRange{toInteger(condt[1]), toInteger(condt[2]), true} + parsedPolicy.Conditions.ContentLengthRange = contentLengthRange{ + Min: toInteger(condt[1]), + Max: toInteger(condt[2]), + Valid: true, + } default: // Condition should be valid. return parsedPolicy, fmt.Errorf("Unknown type %s of conditional field value %s found in POST policy form",