mirror of
https://github.com/minio/minio.git
synced 2025-01-22 20:23:14 -05:00
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:
parent
5197649081
commit
aa98702908
@ -629,6 +629,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrContentSHA256Mismatch
|
||||
case ObjectTooLarge:
|
||||
apiErr = ErrEntityTooLarge
|
||||
case ObjectTooSmall:
|
||||
apiErr = ErrEntityTooSmall
|
||||
default:
|
||||
apiErr = ErrInternalError
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user