api: Handle content-length-range policy properly. (#3297)

content-length-range policy in postPolicy API was
not working properly handle it. The reflection
strategy used has changed in recent version of Go.
Any free form interface{} of any integer is treated
as `float64` this caused a bug where content-length-range
parsing failed to provide any value.

Fixes #3295
master
Harshavardhana 8 years ago committed by GitHub
parent 5197649081
commit aa98702908
  1. 2
      cmd/api-errors.go
  2. 14
      cmd/bucket-handlers.go
  3. 30
      cmd/object-utils.go
  4. 16
      cmd/object-utils_test.go
  5. 94
      cmd/post-policy_test.go
  6. 18
      cmd/postpolicyform.go

@ -629,6 +629,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
apiErr = ErrContentSHA256Mismatch apiErr = ErrContentSHA256Mismatch
case ObjectTooLarge: case ObjectTooLarge:
apiErr = ErrEntityTooLarge apiErr = ErrEntityTooLarge
case ObjectTooSmall:
apiErr = ErrEntityTooSmall
default: default:
apiErr = ErrInternalError apiErr = ErrInternalError
} }

@ -470,14 +470,22 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return 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 lengthRange := postPolicyForm.Conditions.ContentLengthRange
if lengthRange.Valid { if lengthRange.Valid {
// If policy restricted the size of the object. // 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 { } else {
// Default values of min/max size of the object. // Default values of min/max size of the object.
fileBody = limitReader(fileBody, 0, maxObjectSize) fileBody = &rangeReader{
Reader: fileBody,
Min: 0,
Max: maxObjectSize,
}
} }
// Save metadata. // Save metadata.

@ -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) 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 } func (d byBucketName) Less(i, j int) bool { return d[i].Name < d[j].Name }
// Copied from io.LimitReader() // rangeReader returns a Reader that reads from r
// limitReader returns a Reader that reads from r // but returns error after Max bytes read as errDataTooLarge.
// but returns error after n bytes. // but returns error if reader exits before reading Min bytes
// The underlying implementation is a *LimitedReader. // errDataTooSmall.
type limitedReader struct { type rangeReader struct {
R io.Reader // underlying reader Reader io.Reader // underlying reader
M int64 // min bytes remaining Min int64 // min bytes remaining
N int64 // max bytes remaining Max int64 // max bytes remaining
} }
func limitReader(r io.Reader, m, n int64) io.Reader { return &limitedReader{r, m, n} } func (l *rangeReader) Read(p []byte) (n int, err error) {
n, err = l.Reader.Read(p)
func (l *limitedReader) Read(p []byte) (n int, err error) { l.Max -= int64(n)
n, err = l.R.Read(p) l.Min -= int64(n)
l.N -= int64(n) if l.Max < 0 {
l.M -= int64(n)
if l.N < 0 {
// If more data is available than what is expected we return error. // If more data is available than what is expected we return error.
return 0, errDataTooLarge return 0, errDataTooLarge
} }
if err == io.EOF && l.M > 0 { if err == io.EOF && l.Min > 0 {
return 0, errDataTooSmall return 0, errDataTooSmall
} }
return return

@ -116,8 +116,8 @@ func TestIsValidObjectName(t *testing.T) {
} }
} }
// Tests limitReader // Tests rangeReader.
func TestLimitReader(t *testing.T) { func TestRangeReader(t *testing.T) {
testCases := []struct { testCases := []struct {
data string data string
minLen int64 minLen int64
@ -126,15 +126,19 @@ func TestLimitReader(t *testing.T) {
}{ }{
{"1234567890", 0, 15, nil}, {"1234567890", 0, 15, nil},
{"1234567890", 0, 10, nil}, {"1234567890", 0, 10, nil},
{"1234567890", 0, 5, errDataTooLarge}, {"1234567890", 0, 5, toObjectErr(errDataTooLarge, "test", "test")},
{"123", 5, 10, errDataTooSmall}, {"123", 5, 10, toObjectErr(errDataTooSmall, "test", "test")},
{"123", 2, 10, nil}, {"123", 2, 10, nil},
} }
for i, test := range testCases { for i, test := range testCases {
r := strings.NewReader(test.data) r := strings.NewReader(test.data)
_, err := ioutil.ReadAll(limitReader(r, test.minLen, test.maxLen)) _, err := ioutil.ReadAll(&rangeReader{
if err != test.err { 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) t.Fatalf("test %d failed: expected %v, got %v", i+1, test.err, err)
} }
} }

@ -33,6 +33,34 @@ const (
iso8601DateFormat = "20060102T150405Z" 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. // newPostPolicyBytesV4 - creates a bare bones postpolicy string with key and bucket matches.
func newPostPolicyBytesV4(credential, bucketName, objectKey string, expiration time.Time) []byte { func newPostPolicyBytesV4(credential, bucketName, objectKey string, expiration time.Time) []byte {
t := time.Now().UTC() 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) 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: <ERROR> %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. // postPresignSignatureV4 - presigned signature for PostPolicy requests.
@ -267,7 +348,7 @@ func newPostRequestV2(endPoint, bucketName, objectName string, accessKey, secret
return req, nil 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. // Keep time.
t := time.Now().UTC() t := time.Now().UTC()
// Expire the request five minutes from now. // 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) credStr := getCredential(accessKey, serverConfig.GetRegion(), t)
// Create a new post policy. // Create a new post policy.
policy := newPostPolicyBytesV4(credStr, bucketName, objectName, expirationTime) policy := newPostPolicyBytesV4(credStr, bucketName, objectName, expirationTime)
if contentLengthRange {
policy = newPostPolicyBytesV4WithContentRange(credStr, bucketName, objectName, expirationTime)
}
// Only need the encoding. // Only need the encoding.
encodedPolicy := base64.StdEncoding.EncodeToString(policy) 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()) req.Header.Set("Content-Type", w.FormDataContentType())
return req, nil 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)
}

@ -34,10 +34,14 @@ func toString(val interface{}) string {
} }
// toInteger _ Safely convert interface to integer without causing panic. // toInteger _ Safely convert interface to integer without causing panic.
func toInteger(val interface{}) int { func toInteger(val interface{}) int64 {
switch v := val.(type) { switch v := val.(type) {
case int: case float64:
return int64(v)
case int64:
return v return v
case int:
return int64(v)
} }
return 0 return 0
} }
@ -53,8 +57,8 @@ func isString(val interface{}) bool {
// ContentLengthRange - policy content-length-range field. // ContentLengthRange - policy content-length-range field.
type contentLengthRange struct { type contentLengthRange struct {
Min int Min int64
Max int Max int64
Valid bool // If content-length-range was part of policy Valid bool // If content-length-range was part of policy
} }
@ -136,7 +140,11 @@ func parsePostPolicyForm(policy string) (PostPolicyForm, error) {
Value: value, Value: value,
} }
case "content-length-range": 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: default:
// Condition should be valid. // Condition should be valid.
return parsedPolicy, fmt.Errorf("Unknown type %s of conditional field value %s found in POST policy form", return parsedPolicy, fmt.Errorf("Unknown type %s of conditional field value %s found in POST policy form",

Loading…
Cancel
Save