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
This commit is contained in:
Harshavardhana 2016-11-21 04:15:26 -08:00 committed by GitHub
parent 5197649081
commit aa98702908
6 changed files with 143 additions and 31 deletions

View file

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

View file

@ -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.

View file

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

View file

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

View file

@ -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: <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.
@ -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)
}

View file

@ -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",