diff --git a/functional_tests.go b/functional_tests.go index 714bb3cc9..a3dbc49c9 100644 --- a/functional_tests.go +++ b/functional_tests.go @@ -5643,9 +5643,6 @@ func testPresignedPostPolicy() { "policy": "", } - // Seed random based on current time. - rand.Seed(time.Now().Unix()) - // Instantiate new minio client object c, err := minio.New(os.Getenv(serverEndpoint), &minio.Options{ @@ -5692,38 +5689,13 @@ func testPresignedPostPolicy() { } policy := minio.NewPostPolicy() - - if err := policy.SetBucket(""); err == nil { - logError(testName, function, args, startTime, "", "SetBucket did not fail for invalid conditions", err) - return - } - if err := policy.SetKey(""); err == nil { - logError(testName, function, args, startTime, "", "SetKey did not fail for invalid conditions", err) - return - } - if err := policy.SetExpires(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)); err == nil { - logError(testName, function, args, startTime, "", "SetExpires did not fail for invalid conditions", err) - return - } - if err := policy.SetContentType(""); err == nil { - logError(testName, function, args, startTime, "", "SetContentType did not fail for invalid conditions", err) - return - } - if err := policy.SetContentLengthRange(1024*1024, 1024); err == nil { - logError(testName, function, args, startTime, "", "SetContentLengthRange did not fail for invalid conditions", err) - return - } - if err := policy.SetUserMetadata("", ""); err == nil { - logError(testName, function, args, startTime, "", "SetUserMetadata did not fail for invalid conditions", err) - return - } - policy.SetBucket(bucketName) policy.SetKey(objectName) policy.SetExpires(time.Now().UTC().AddDate(0, 0, 10)) // expires in 10 days policy.SetContentType("binary/octet-stream") policy.SetContentLengthRange(10, 1024*1024) policy.SetUserMetadata(metadataKey, metadataValue) + policy.SetContentEncoding("gzip") // Add CRC32C checksum := minio.ChecksumCRC32C.ChecksumBytes(buf) @@ -5865,6 +5837,186 @@ func testPresignedPostPolicy() { logSuccess(testName, function, args, startTime) } +// testPresignedPostPolicyWrongFile tests that when we have a policy with a checksum, we cannot POST the wrong file +func testPresignedPostPolicyWrongFile() { + // initialize logging params + startTime := time.Now() + testName := getFuncName() + function := "PresignedPostPolicy(policy)" + args := map[string]interface{}{ + "policy": "", + } + + // Instantiate new minio client object + c, err := minio.New(os.Getenv(serverEndpoint), + &minio.Options{ + Creds: credentials.NewStaticV4(os.Getenv(accessKey), os.Getenv(secretKey), ""), + Transport: createHTTPTransport(), + Secure: mustParseBool(os.Getenv(enableHTTPS)), + }) + if err != nil { + logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) + return + } + + // Enable tracing, write to stderr. + // c.TraceOn(os.Stderr) + + // Set user agent. + c.SetAppInfo("MinIO-go-FunctionalTest", appVersion) + + // Generate a new random bucket name. + bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") + + // Make a new bucket in 'us-east-1' (source bucket). + err = c.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{Region: "us-east-1"}) + if err != nil { + logError(testName, function, args, startTime, "", "MakeBucket failed", err) + return + } + + defer cleanupBucket(bucketName, c) + + // Generate 33K of data. + reader := getDataReader("datafile-33-kB") + defer reader.Close() + + objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "") + // Azure requires the key to not start with a number + metadataKey := randString(60, rand.NewSource(time.Now().UnixNano()), "user") + metadataValue := randString(60, rand.NewSource(time.Now().UnixNano()), "") + + buf, err := io.ReadAll(reader) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAll failed", err) + return + } + + policy := minio.NewPostPolicy() + policy.SetBucket(bucketName) + policy.SetKey(objectName) + policy.SetExpires(time.Now().UTC().AddDate(0, 0, 10)) // expires in 10 days + policy.SetContentType("binary/octet-stream") + policy.SetContentLengthRange(10, 1024*1024) + policy.SetUserMetadata(metadataKey, metadataValue) + + // Add CRC32C of the 33kB file that the policy will explicitly allow. + checksum := minio.ChecksumCRC32C.ChecksumBytes(buf) + err = policy.SetChecksum(checksum) + if err != nil { + logError(testName, function, args, startTime, "", "SetChecksum failed", err) + return + } + + args["policy"] = policy.String() + + presignedPostPolicyURL, formData, err := c.PresignedPostPolicy(context.Background(), policy) + if err != nil { + logError(testName, function, args, startTime, "", "PresignedPostPolicy failed", err) + return + } + + // At this stage, we have a policy that allows us to upload datafile-33-kB. + // Test that uploading datafile-10-kB, with a different checksum, fails as expected + filePath := getMintDataDirFilePath("datafile-10-kB") + if filePath == "" { + // Make a temp file with 10 KB data. + file, err := os.CreateTemp(os.TempDir(), "PresignedPostPolicyTest") + if err != nil { + logError(testName, function, args, startTime, "", "TempFile creation failed", err) + return + } + if _, err = io.Copy(file, getDataReader("datafile-10-kB")); err != nil { + logError(testName, function, args, startTime, "", "Copy failed", err) + return + } + if err = file.Close(); err != nil { + logError(testName, function, args, startTime, "", "File Close failed", err) + return + } + filePath = file.Name() + } + fileReader := getDataReader("datafile-10-kB") + defer fileReader.Close() + buf10k, err := io.ReadAll(fileReader) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAll failed", err) + return + } + otherChecksum := minio.ChecksumCRC32C.ChecksumBytes(buf10k) + + var formBuf bytes.Buffer + writer := multipart.NewWriter(&formBuf) + for k, v := range formData { + if k == "x-amz-checksum-crc32c" { + v = otherChecksum.Encoded() + } + writer.WriteField(k, v) + } + + // Add file to post request + f, err := os.Open(filePath) + defer f.Close() + if err != nil { + logError(testName, function, args, startTime, "", "File open failed", err) + return + } + w, err := writer.CreateFormFile("file", filePath) + if err != nil { + logError(testName, function, args, startTime, "", "CreateFormFile failed", err) + return + } + _, err = io.Copy(w, f) + if err != nil { + logError(testName, function, args, startTime, "", "Copy failed", err) + return + } + writer.Close() + + httpClient := &http.Client{ + Timeout: 30 * time.Second, + Transport: createHTTPTransport(), + } + args["url"] = presignedPostPolicyURL.String() + + req, err := http.NewRequest(http.MethodPost, presignedPostPolicyURL.String(), bytes.NewReader(formBuf.Bytes())) + if err != nil { + logError(testName, function, args, startTime, "", "HTTP request failed", err) + return + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + // Make the POST request with the form data. + res, err := httpClient.Do(req) + if err != nil { + logError(testName, function, args, startTime, "", "HTTP request failed", err) + return + } + defer res.Body.Close() + if res.StatusCode != http.StatusForbidden { + logError(testName, function, args, startTime, "", "HTTP request unexpected status", errors.New(res.Status)) + return + } + + // Read the response body, ensure it has checksum failure message + resBody, err := io.ReadAll(res.Body) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAll failed", err) + return + } + + // Normalize the response body, because S3 uses quotes around the policy condition components + // in the error message, MinIO does not. + resBodyStr := strings.ReplaceAll(string(resBody), `"`, "") + if !strings.Contains(resBodyStr, "Policy Condition failed: [eq, $x-amz-checksum-crc32c, aHnJMw==]") { + logError(testName, function, args, startTime, "", "Unexpected response body", errors.New(resBodyStr)) + return + } + + logSuccess(testName, function, args, startTime) +} + // Tests copy object func testCopyObject() { // initialize logging params @@ -14977,6 +15129,7 @@ func main() { testGetObjectReadAtFunctional() testGetObjectReadAtWhenEOFWasReached() testPresignedPostPolicy() + testPresignedPostPolicyWrongFile() testCopyObject() testComposeObjectErrorCases() testCompose10KSources() diff --git a/post-policy.go b/post-policy.go index 1b01f4101..26bf441b5 100644 --- a/post-policy.go +++ b/post-policy.go @@ -85,7 +85,7 @@ func (p *PostPolicy) SetExpires(t time.Time) error { // SetKey - Sets an object name for the policy based upload. func (p *PostPolicy) SetKey(key string) error { - if strings.TrimSpace(key) == "" || key == "" { + if strings.TrimSpace(key) == "" { return errInvalidArgument("Object name is empty.") } policyCond := policyCondition{ @@ -118,7 +118,7 @@ func (p *PostPolicy) SetKeyStartsWith(keyStartsWith string) error { // SetBucket - Sets bucket at which objects will be uploaded to. func (p *PostPolicy) SetBucket(bucketName string) error { - if strings.TrimSpace(bucketName) == "" || bucketName == "" { + if strings.TrimSpace(bucketName) == "" { return errInvalidArgument("Bucket name is empty.") } policyCond := policyCondition{ @@ -135,7 +135,7 @@ func (p *PostPolicy) SetBucket(bucketName string) error { // SetCondition - Sets condition for credentials, date and algorithm func (p *PostPolicy) SetCondition(matchType, condition, value string) error { - if strings.TrimSpace(value) == "" || value == "" { + if strings.TrimSpace(value) == "" { return errInvalidArgument("No value specified for condition") } @@ -156,7 +156,7 @@ func (p *PostPolicy) SetCondition(matchType, condition, value string) error { // SetTagging - Sets tagging for the object for this policy based upload. func (p *PostPolicy) SetTagging(tagging string) error { - if strings.TrimSpace(tagging) == "" || tagging == "" { + if strings.TrimSpace(tagging) == "" { return errInvalidArgument("No tagging specified.") } _, err := tags.ParseObjectXML(strings.NewReader(tagging)) @@ -178,7 +178,7 @@ func (p *PostPolicy) SetTagging(tagging string) error { // SetContentType - Sets content-type of the object for this policy // based upload. func (p *PostPolicy) SetContentType(contentType string) error { - if strings.TrimSpace(contentType) == "" || contentType == "" { + if strings.TrimSpace(contentType) == "" { return errInvalidArgument("No content type specified.") } policyCond := policyCondition{ @@ -211,7 +211,7 @@ func (p *PostPolicy) SetContentTypeStartsWith(contentTypeStartsWith string) erro // SetContentDisposition - Sets content-disposition of the object for this policy func (p *PostPolicy) SetContentDisposition(contentDisposition string) error { - if strings.TrimSpace(contentDisposition) == "" || contentDisposition == "" { + if strings.TrimSpace(contentDisposition) == "" { return errInvalidArgument("No content disposition specified.") } policyCond := policyCondition{ @@ -226,27 +226,44 @@ func (p *PostPolicy) SetContentDisposition(contentDisposition string) error { return nil } +// SetContentEncoding - Sets content-encoding of the object for this policy +func (p *PostPolicy) SetContentEncoding(contentEncoding string) error { + if strings.TrimSpace(contentEncoding) == "" { + return errInvalidArgument("No content encoding specified.") + } + policyCond := policyCondition{ + matchType: "eq", + condition: "$Content-Encoding", + value: contentEncoding, + } + if err := p.addNewPolicy(policyCond); err != nil { + return err + } + p.formData["Content-Encoding"] = contentEncoding + return nil +} + // SetContentLengthRange - Set new min and max content length // condition for all incoming uploads. -func (p *PostPolicy) SetContentLengthRange(min, max int64) error { - if min > max { +func (p *PostPolicy) SetContentLengthRange(minLen, maxLen int64) error { + if minLen > maxLen { return errInvalidArgument("Minimum limit is larger than maximum limit.") } - if min < 0 { + if minLen < 0 { return errInvalidArgument("Minimum limit cannot be negative.") } - if max <= 0 { + if maxLen <= 0 { return errInvalidArgument("Maximum limit cannot be non-positive.") } - p.contentLengthRange.min = min - p.contentLengthRange.max = max + p.contentLengthRange.min = minLen + p.contentLengthRange.max = maxLen return nil } // SetSuccessActionRedirect - Sets the redirect success url of the object for this policy // based upload. func (p *PostPolicy) SetSuccessActionRedirect(redirect string) error { - if strings.TrimSpace(redirect) == "" || redirect == "" { + if strings.TrimSpace(redirect) == "" { return errInvalidArgument("Redirect is empty") } policyCond := policyCondition{ @@ -264,7 +281,7 @@ func (p *PostPolicy) SetSuccessActionRedirect(redirect string) error { // SetSuccessStatusAction - Sets the status success code of the object for this policy // based upload. func (p *PostPolicy) SetSuccessStatusAction(status string) error { - if strings.TrimSpace(status) == "" || status == "" { + if strings.TrimSpace(status) == "" { return errInvalidArgument("Status is empty") } policyCond := policyCondition{ @@ -282,10 +299,10 @@ func (p *PostPolicy) SetSuccessStatusAction(status string) error { // SetUserMetadata - Set user metadata as a key/value couple. // Can be retrieved through a HEAD request or an event. func (p *PostPolicy) SetUserMetadata(key, value string) error { - if strings.TrimSpace(key) == "" || key == "" { + if strings.TrimSpace(key) == "" { return errInvalidArgument("Key is empty") } - if strings.TrimSpace(value) == "" || value == "" { + if strings.TrimSpace(value) == "" { return errInvalidArgument("Value is empty") } headerName := fmt.Sprintf("x-amz-meta-%s", key) @@ -304,7 +321,7 @@ func (p *PostPolicy) SetUserMetadata(key, value string) error { // SetUserMetadataStartsWith - Set how an user metadata should starts with. // Can be retrieved through a HEAD request or an event. func (p *PostPolicy) SetUserMetadataStartsWith(key, value string) error { - if strings.TrimSpace(key) == "" || key == "" { + if strings.TrimSpace(key) == "" { return errInvalidArgument("Key is empty") } headerName := fmt.Sprintf("x-amz-meta-%s", key) @@ -326,8 +343,6 @@ func (p *PostPolicy) SetChecksum(c Checksum) error { p.formData[amzChecksumAlgo] = c.Type.String() p.formData[c.Type.Key()] = c.Encoded() - // Needed for S3 compatibility. MinIO ignores the checksum keys in the policy. - // https://github.com/minio/minio/blob/RELEASE.2024-08-29T01-40-52Z/cmd/postpolicyform.go#L60-L65 policyCond := policyCondition{ matchType: "eq", condition: fmt.Sprintf("$%s", amzChecksumAlgo), diff --git a/post-policy_test.go b/post-policy_test.go new file mode 100644 index 000000000..c105e053c --- /dev/null +++ b/post-policy_test.go @@ -0,0 +1,459 @@ +/* + * MinIO Go Library for Amazon S3 Compatible Cloud Storage + * Copyright 2015-2023 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. + */ + +package minio + +import ( + "strings" + "testing" + "time" + + "github.com/minio/minio-go/v7/pkg/encrypt" +) + +func TestPostPolicySetExpires(t *testing.T) { + tests := []struct { + name string + input time.Time + wantErr bool + wantResult string + }{ + { + name: "valid time", + input: time.Date(2023, time.March, 2, 15, 4, 5, 0, time.UTC), + wantErr: false, + wantResult: "2023-03-02T15:04:05", + }, + { + name: "time before 1970", + input: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pp := NewPostPolicy() + + err := pp.SetExpires(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("%s: want error: %v, got: %v", tt.name, tt.wantErr, err) + } + + if tt.wantResult != "" { + result := pp.String() + if !strings.Contains(result, tt.wantResult) { + t.Errorf("%s: want result to contain: '%s', got: '%s'", tt.name, tt.wantResult, result) + } + } + }) + } +} + +func TestPostPolicySetKey(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + wantResult string + }{ + { + name: "valid key", + input: "my-object", + wantResult: `"eq","$key","my-object"`, + }, + { + name: "empty key", + input: "", + wantErr: true, + }, + { + name: "key with spaces", + input: " ", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pp := NewPostPolicy() + + err := pp.SetKey(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("%s: want error: %v, got: %v", tt.name, tt.wantErr, err) + } + + if tt.wantResult != "" { + result := pp.String() + if !strings.Contains(result, tt.wantResult) { + t.Errorf("%s: want result to contain: '%s', got: '%s'", tt.name, tt.wantResult, result) + } + } + }) + } +} + +func TestPostPolicySetKeyStartsWith(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "valid key prefix", + input: "my-prefix/", + want: `["starts-with","$key","my-prefix/"]`, + }, + { + name: "empty prefix (allow any key)", + input: "", + want: `["starts-with","$key",""]`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pp := NewPostPolicy() + + err := pp.SetKeyStartsWith(tt.input) + if err != nil { + t.Errorf("%s: want no error, got: %v", tt.name, err) + } + + if tt.want != "" { + result := pp.String() + if !strings.Contains(result, tt.want) { + t.Errorf("%s: want result to contain: '%s', got: '%s'", tt.name, tt.want, result) + } + } + }) + } +} + +func TestPostPolicySetBucket(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + wantResult string + }{ + { + name: "valid bucket", + input: "my-bucket", + wantResult: `"eq","$bucket","my-bucket"`, + }, + { + name: "empty bucket", + input: "", + wantErr: true, + }, + { + name: "bucket with spaces", + input: " ", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pp := NewPostPolicy() + + err := pp.SetBucket(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("%s: want error: %v, got: %v", tt.name, tt.wantErr, err) + } + + if tt.wantResult != "" { + result := pp.String() + if !strings.Contains(result, tt.wantResult) { + t.Errorf("%s: want result to contain: '%s', got: '%s'", tt.name, tt.wantResult, result) + } + } + }) + } +} + +func TestPostPolicySetCondition(t *testing.T) { + tests := []struct { + name string + matchType string + condition string + value string + wantErr bool + wantResult string + }{ + { + name: "valid eq condition", + matchType: "eq", + condition: "X-Amz-Date", + value: "20210324T000000Z", + wantResult: `"eq","$X-Amz-Date","20210324T000000Z"`, + }, + { + name: "empty value", + matchType: "eq", + condition: "X-Amz-Date", + value: "", + wantErr: true, + }, + { + name: "invalid condition", + matchType: "eq", + condition: "Invalid-Condition", + value: "somevalue", + wantErr: true, + }, + { + name: "valid starts-with condition", + matchType: "starts-with", + condition: "X-Amz-Credential", + value: "my-access-key", + wantResult: `"starts-with","$X-Amz-Credential","my-access-key"`, + }, + { + name: "empty condition", + matchType: "eq", + condition: "", + value: "somevalue", + wantErr: true, + }, + { + name: "empty matchType", + matchType: "", + condition: "X-Amz-Date", + value: "somevalue", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pp := NewPostPolicy() + + err := pp.SetCondition(tt.matchType, tt.condition, tt.value) + if (err != nil) != tt.wantErr { + t.Errorf("%s: want error: %v, got: %v", tt.name, tt.wantErr, err) + } + + if tt.wantResult != "" { + result := pp.String() + if !strings.Contains(result, tt.wantResult) { + t.Errorf("%s: want result to contain: '%s', got: '%s'", tt.name, tt.wantResult, result) + } + } + }) + } +} + +func TestPostPolicySetTagging(t *testing.T) { + tests := []struct { + name string + tagging string + wantErr bool + wantResult string + }{ + { + name: "valid tagging", + tagging: `<Tagging><TagSet><Tag><Key>key1</Key><Value>value1</Value></Tag></TagSet></Tagging>`, + wantResult: `"eq","$tagging","<Tagging><TagSet><Tag><Key>key1</Key><Value>value1</Value></Tag></TagSet></Tagging>"`, + }, + { + name: "empty tagging", + tagging: "", + wantErr: true, + }, + { + name: "whitespace tagging", + tagging: " ", + wantErr: true, + }, + { + name: "invalid XML", + tagging: `<Tagging><TagSet><Tag><Key>key1</Key><Value>value1</Value></Tag></TagSet>`, + wantErr: true, + }, + { + name: "invalid schema", + tagging: `<InvalidTagging></InvalidTagging>`, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pp := NewPostPolicy() + + err := pp.SetTagging(tt.tagging) + if (err != nil) != tt.wantErr { + t.Errorf("%s: want error: %v, got: %v", tt.name, tt.wantErr, err) + } + + if tt.wantResult != "" { + result := pp.String() + if !strings.Contains(result, tt.wantResult) { + t.Errorf("%s: want result to contain: '%s', got: '%s'", tt.name, tt.wantResult, result) + } + } + }) + } +} + +func TestPostPolicySetUserMetadata(t *testing.T) { + tests := []struct { + name string + key string + value string + wantErr bool + wantResult string + }{ + { + name: "valid metadata", + key: "user-key", + value: "user-value", + wantResult: `"eq","$x-amz-meta-user-key","user-value"`, + }, + { + name: "empty key", + key: "", + value: "somevalue", + wantErr: true, + }, + { + name: "empty value", + key: "user-key", + value: "", + wantErr: true, + }, + { + name: "key with spaces", + key: " ", + value: "somevalue", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pp := NewPostPolicy() + + err := pp.SetUserMetadata(tt.key, tt.value) + if (err != nil) != tt.wantErr { + t.Errorf("%s: want error: %v, got: %v", tt.name, tt.wantErr, err) + } + + if tt.wantResult != "" { + result := pp.String() + if !strings.Contains(result, tt.wantResult) { + t.Errorf("%s: want result to contain: '%s', got: '%s'", tt.name, tt.wantResult, result) + } + } + }) + } +} + +func TestPostPolicySetChecksum(t *testing.T) { + tests := []struct { + name string + checksum Checksum + wantErr bool + wantResult string + }{ + { + name: "valid checksum SHA256", + checksum: ChecksumSHA256.ChecksumBytes([]byte("somerandomdata")), + wantResult: `[["eq","$x-amz-checksum-algorithm","SHA256"],["eq","$x-amz-checksum-sha256","29/7Qm/iMzZ1O3zMbO0luv6mYWyS6JIqPYV9lc8w1PA="]]`, + }, + { + name: "valid checksum CRC32", + checksum: ChecksumCRC32.ChecksumBytes([]byte("somerandomdata")), + wantResult: `[["eq","$x-amz-checksum-algorithm","CRC32"],["eq","$x-amz-checksum-crc32","7sOPnw=="]]`, + }, + { + name: "empty checksum", + checksum: Checksum{}, + wantResult: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pp := NewPostPolicy() + + err := pp.SetChecksum(tt.checksum) + if (err != nil) != tt.wantErr { + t.Errorf("%s: want error: %v, got: %v", tt.name, tt.wantErr, err) + } + + if tt.wantResult != "" { + result := pp.String() + if !strings.Contains(result, tt.wantResult) { + t.Errorf("%s: want result to contain: '%s', got: '%s'", tt.name, tt.wantResult, result) + } + } + }) + } +} + +func TestPostPolicySetEncryption(t *testing.T) { + tests := []struct { + name string + sseType string + keyID string + want map[string]string + }{ + { + name: "SSE-S3 encryption", + sseType: "SSE-S3", + keyID: "my-key-id", + want: map[string]string{ + "X-Amz-Server-Side-Encryption": "aws:kms", + "X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": "my-key-id", + }, + }, + { + name: "SSE-C encryption with Key ID", + sseType: "SSE-C", + keyID: "my-key-id", + want: map[string]string{ + "X-Amz-Server-Side-Encryption-Customer-Key": "bXktc2VjcmV0LWtleTEyMzQ1Njc4OTBhYmNkZWZnaGk=", + "X-Amz-Server-Side-Encryption-Customer-Key-Md5": "T1mefJwyXBH43sRtfEgRZQ==", + "X-Amz-Server-Side-Encryption-Customer-Algorithm": "AES256", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pp := NewPostPolicy() + + var sse encrypt.ServerSide + var err error + if tt.sseType == "SSE-S3" { + sse, err = encrypt.NewSSEKMS(tt.keyID, nil) + if err != nil { + t.Fatalf("Failed to create SSE-KMS: %v", err) + } + } else if tt.sseType == "SSE-C" { + sse, err = encrypt.NewSSEC([]byte("my-secret-key1234567890abcdefghi")) + if err != nil { + t.Fatalf("Failed to create SSE-C: %v", err) + } + } else { + t.Fatalf("Unknown SSE type: %s", tt.sseType) + } + + pp.SetEncryption(sse) + + for k, v := range tt.want { + if pp.formData[k] != v { + t.Errorf("%s: want %s: %s, got: %s", tt.name, k, v, pp.formData[k]) + } + } + }) + } +} diff --git a/retry-continous.go b/retry-continous.go index bfeea95f3..81fcf16f1 100644 --- a/retry-continous.go +++ b/retry-continous.go @@ -20,7 +20,7 @@ package minio import "time" // newRetryTimerContinous creates a timer with exponentially increasing delays forever. -func (c *Client) newRetryTimerContinous(unit, cap time.Duration, jitter float64, doneCh chan struct{}) <-chan int { +func (c *Client) newRetryTimerContinous(baseSleep, maxSleep time.Duration, jitter float64, doneCh chan struct{}) <-chan int { attemptCh := make(chan int) // normalize jitter to the range [0, 1.0] @@ -39,10 +39,10 @@ func (c *Client) newRetryTimerContinous(unit, cap time.Duration, jitter float64, if attempt > maxAttempt { attempt = maxAttempt } - // sleep = random_between(0, min(cap, base * 2 ** attempt)) - sleep := unit * time.Duration(1<<uint(attempt)) - if sleep > cap { - sleep = cap + // sleep = random_between(0, min(maxSleep, base * 2 ** attempt)) + sleep := baseSleep * time.Duration(1<<uint(attempt)) + if sleep > maxSleep { + sleep = maxSleep } if jitter != NoJitter { sleep -= time.Duration(c.random.Float64() * float64(sleep) * jitter) diff --git a/retry.go b/retry.go index d15eb5901..4cc45920c 100644 --- a/retry.go +++ b/retry.go @@ -45,7 +45,7 @@ var DefaultRetryCap = time.Second // newRetryTimer creates a timer with exponentially increasing // delays until the maximum retry attempts are reached. -func (c *Client) newRetryTimer(ctx context.Context, maxRetry int, unit, cap time.Duration, jitter float64) <-chan int { +func (c *Client) newRetryTimer(ctx context.Context, maxRetry int, baseSleep, maxSleep time.Duration, jitter float64) <-chan int { attemptCh := make(chan int) // computes the exponential backoff duration according to @@ -59,10 +59,10 @@ func (c *Client) newRetryTimer(ctx context.Context, maxRetry int, unit, cap time jitter = MaxJitter } - // sleep = random_between(0, min(cap, base * 2 ** attempt)) - sleep := unit * time.Duration(1<<uint(attempt)) - if sleep > cap { - sleep = cap + // sleep = random_between(0, min(maxSleep, base * 2 ** attempt)) + sleep := baseSleep * time.Duration(1<<uint(attempt)) + if sleep > maxSleep { + sleep = maxSleep } if jitter != NoJitter { sleep -= time.Duration(c.random.Float64() * float64(sleep) * jitter)