storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/post-policy_test.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2016, 2017, 2018 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package cmd 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/base64" 23 "fmt" 24 "io/ioutil" 25 "mime/multipart" 26 "net/http" 27 "net/http/httptest" 28 "net/url" 29 "testing" 30 "time" 31 32 humanize "github.com/dustin/go-humanize" 33 ) 34 35 const ( 36 iso8601DateFormat = "20060102T150405Z" 37 ) 38 39 func newPostPolicyBytesV4WithContentRange(credential, bucketName, objectKey string, expiration time.Time) []byte { 40 t := UTCNow() 41 // Add the expiration date. 42 expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(iso8601TimeFormat)) 43 // Add the bucket condition, only accept buckets equal to the one passed. 44 bucketConditionStr := fmt.Sprintf(`["eq", "$bucket", "%s"]`, bucketName) 45 // Add the key condition, only accept keys equal to the one passed. 46 keyConditionStr := fmt.Sprintf(`["eq", "$key", "%s/upload.txt"]`, objectKey) 47 // Add content length condition, only accept content sizes of a given length. 48 contentLengthCondStr := `["content-length-range", 1024, 1048576]` 49 // Add the algorithm condition, only accept AWS SignV4 Sha256. 50 algorithmConditionStr := `["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"]` 51 // Add the date condition, only accept the current date. 52 dateConditionStr := fmt.Sprintf(`["eq", "$x-amz-date", "%s"]`, t.Format(iso8601DateFormat)) 53 // Add the credential string, only accept the credential passed. 54 credentialConditionStr := fmt.Sprintf(`["eq", "$x-amz-credential", "%s"]`, credential) 55 // Add the meta-uuid string, set to 1234 56 uuidConditionStr := fmt.Sprintf(`["eq", "$x-amz-meta-uuid", "%s"]`, "1234") 57 58 // Combine all conditions into one string. 59 conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s, %s]`, bucketConditionStr, 60 keyConditionStr, contentLengthCondStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr) 61 retStr := "{" 62 retStr = retStr + expirationStr + "," 63 retStr = retStr + conditionStr 64 retStr = retStr + "}" 65 66 return []byte(retStr) 67 } 68 69 // newPostPolicyBytesV4 - creates a bare bones postpolicy string with key and bucket matches. 70 func newPostPolicyBytesV4(credential, bucketName, objectKey string, expiration time.Time) []byte { 71 t := UTCNow() 72 // Add the expiration date. 73 expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(iso8601TimeFormat)) 74 // Add the bucket condition, only accept buckets equal to the one passed. 75 bucketConditionStr := fmt.Sprintf(`["eq", "$bucket", "%s"]`, bucketName) 76 // Add the key condition, only accept keys equal to the one passed. 77 keyConditionStr := fmt.Sprintf(`["eq", "$key", "%s/upload.txt"]`, objectKey) 78 // Add the algorithm condition, only accept AWS SignV4 Sha256. 79 algorithmConditionStr := `["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"]` 80 // Add the date condition, only accept the current date. 81 dateConditionStr := fmt.Sprintf(`["eq", "$x-amz-date", "%s"]`, t.Format(iso8601DateFormat)) 82 // Add the credential string, only accept the credential passed. 83 credentialConditionStr := fmt.Sprintf(`["eq", "$x-amz-credential", "%s"]`, credential) 84 // Add the meta-uuid string, set to 1234 85 uuidConditionStr := fmt.Sprintf(`["eq", "$x-amz-meta-uuid", "%s"]`, "1234") 86 87 // Combine all conditions into one string. 88 conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s]`, bucketConditionStr, keyConditionStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr) 89 retStr := "{" 90 retStr = retStr + expirationStr + "," 91 retStr = retStr + conditionStr 92 retStr = retStr + "}" 93 94 return []byte(retStr) 95 } 96 97 // newPostPolicyBytesV2 - creates a bare bones postpolicy string with key and bucket matches. 98 func newPostPolicyBytesV2(bucketName, objectKey string, expiration time.Time) []byte { 99 // Add the expiration date. 100 expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(iso8601TimeFormat)) 101 // Add the bucket condition, only accept buckets equal to the one passed. 102 bucketConditionStr := fmt.Sprintf(`["eq", "$bucket", "%s"]`, bucketName) 103 // Add the key condition, only accept keys equal to the one passed. 104 keyConditionStr := fmt.Sprintf(`["starts-with", "$key", "%s/upload.txt"]`, objectKey) 105 106 // Combine all conditions into one string. 107 conditionStr := fmt.Sprintf(`"conditions":[%s, %s]`, bucketConditionStr, keyConditionStr) 108 retStr := "{" 109 retStr = retStr + expirationStr + "," 110 retStr = retStr + conditionStr 111 retStr = retStr + "}" 112 113 return []byte(retStr) 114 } 115 116 // Wrapper for calling TestPostPolicyBucketHandler tests for both Erasure multiple disks and single node setup. 117 func TestPostPolicyBucketHandler(t *testing.T) { 118 ExecObjectLayerTest(t, testPostPolicyBucketHandler) 119 } 120 121 // testPostPolicyBucketHandler - Tests validate post policy handler uploading objects. 122 func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErrHandler) { 123 if err := newTestConfig(globalMinioDefaultRegion, obj); err != nil { 124 t.Fatalf("Initializing config.json failed") 125 } 126 127 // get random bucket name. 128 bucketName := getRandomBucketName() 129 130 var opts ObjectOptions 131 // Register the API end points with Erasure/FS object layer. 132 apiRouter := initTestAPIEndPoints(obj, []string{"PostPolicy"}) 133 134 credentials := globalActiveCred 135 136 curTime := UTCNow() 137 curTimePlus5Min := curTime.Add(time.Minute * 5) 138 139 // bucketnames[0]. 140 // objectNames[0]. 141 // uploadIds [0]. 142 // Create bucket before initiating NewMultipartUpload. 143 err := obj.MakeBucketWithLocation(context.Background(), bucketName, BucketOptions{}) 144 if err != nil { 145 // Failed to create newbucket, abort. 146 t.Fatalf("%s : %s", instanceType, err.Error()) 147 } 148 149 // Test cases for signature-V2. 150 testCasesV2 := []struct { 151 expectedStatus int 152 accessKey string 153 secretKey string 154 }{ 155 {http.StatusForbidden, "invalidaccesskey", credentials.SecretKey}, 156 {http.StatusForbidden, credentials.AccessKey, "invalidsecretkey"}, 157 {http.StatusNoContent, credentials.AccessKey, credentials.SecretKey}, 158 } 159 160 for i, test := range testCasesV2 { 161 // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler. 162 rec := httptest.NewRecorder() 163 req, perr := newPostRequestV2("", bucketName, "testobject", test.accessKey, test.secretKey) 164 if perr != nil { 165 t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", i+1, instanceType, perr) 166 } 167 // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler. 168 // Call the ServeHTTP to execute the handler. 169 apiRouter.ServeHTTP(rec, req) 170 if rec.Code != test.expectedStatus { 171 t.Fatalf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, test.expectedStatus, rec.Code) 172 } 173 } 174 175 // Test cases for signature-V4. 176 testCasesV4 := []struct { 177 objectName string 178 data []byte 179 expectedHeaders map[string]string 180 expectedRespStatus int 181 accessKey string 182 secretKey string 183 malformedBody bool 184 }{ 185 // Success case. 186 { 187 objectName: "test", 188 data: []byte("Hello, World"), 189 expectedRespStatus: http.StatusNoContent, 190 expectedHeaders: map[string]string{"X-Amz-Meta-Uuid": "1234"}, 191 accessKey: credentials.AccessKey, 192 secretKey: credentials.SecretKey, 193 malformedBody: false, 194 }, 195 // Bad case invalid request. 196 { 197 objectName: "test", 198 data: []byte("Hello, World"), 199 expectedRespStatus: http.StatusForbidden, 200 accessKey: "", 201 secretKey: "", 202 malformedBody: false, 203 }, 204 // Bad case malformed input. 205 { 206 objectName: "test", 207 data: []byte("Hello, World"), 208 expectedRespStatus: http.StatusBadRequest, 209 accessKey: credentials.AccessKey, 210 secretKey: credentials.SecretKey, 211 malformedBody: true, 212 }, 213 } 214 215 for i, testCase := range testCasesV4 { 216 // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler. 217 rec := httptest.NewRecorder() 218 req, perr := newPostRequestV4("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, testCase.secretKey) 219 if perr != nil { 220 t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", i+1, instanceType, perr) 221 } 222 if testCase.malformedBody { 223 // Change the request body. 224 req.Body = ioutil.NopCloser(bytes.NewReader([]byte("Hello,"))) 225 } 226 // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler. 227 // Call the ServeHTTP to execute the handler. 228 apiRouter.ServeHTTP(rec, req) 229 if rec.Code != testCase.expectedRespStatus { 230 t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code) 231 } 232 // When the operation is successful, check if sending metadata is successful too 233 if rec.Code == http.StatusNoContent { 234 objInfo, err := obj.GetObjectInfo(context.Background(), bucketName, testCase.objectName+"/upload.txt", opts) 235 if err != nil { 236 t.Error("Unexpected error: ", err) 237 } 238 for k, v := range testCase.expectedHeaders { 239 if objInfo.UserDefined[k] != v { 240 t.Errorf("Expected to have header %s with value %s, but found value `%s` instead", k, v, objInfo.UserDefined[k]) 241 } 242 } 243 } 244 } 245 246 region := "us-east-1" 247 // Test cases for signature-V4. 248 testCasesV4BadData := []struct { 249 objectName string 250 data []byte 251 expectedRespStatus int 252 accessKey string 253 secretKey string 254 dates []interface{} 255 policy string 256 corruptedBase64 bool 257 corruptedMultipart bool 258 }{ 259 // Success case. 260 { 261 objectName: "test", 262 data: []byte("Hello, World"), 263 expectedRespStatus: http.StatusNoContent, 264 accessKey: credentials.AccessKey, 265 secretKey: credentials.SecretKey, 266 dates: []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)}, 267 policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]}`, 268 }, 269 // Corrupted Base 64 result 270 { 271 objectName: "test", 272 data: []byte("Hello, World"), 273 expectedRespStatus: http.StatusBadRequest, 274 accessKey: credentials.AccessKey, 275 secretKey: credentials.SecretKey, 276 dates: []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)}, 277 policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"]]}`, 278 corruptedBase64: true, 279 }, 280 // Corrupted Multipart body 281 { 282 objectName: "test", 283 data: []byte("Hello, World"), 284 expectedRespStatus: http.StatusBadRequest, 285 accessKey: credentials.AccessKey, 286 secretKey: credentials.SecretKey, 287 dates: []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)}, 288 policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"]]}`, 289 corruptedMultipart: true, 290 }, 291 292 // Bad case invalid request. 293 { 294 objectName: "test", 295 data: []byte("Hello, World"), 296 expectedRespStatus: http.StatusForbidden, 297 accessKey: "", 298 secretKey: "", 299 dates: []interface{}{}, 300 policy: ``, 301 }, 302 // Expired document 303 { 304 objectName: "test", 305 data: []byte("Hello, World"), 306 expectedRespStatus: http.StatusForbidden, 307 accessKey: credentials.AccessKey, 308 secretKey: credentials.SecretKey, 309 dates: []interface{}{curTime.Add(-1 * time.Minute * 5).Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)}, 310 policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"]]}`, 311 }, 312 // Corrupted policy document 313 { 314 objectName: "test", 315 data: []byte("Hello, World"), 316 expectedRespStatus: http.StatusForbidden, 317 accessKey: credentials.AccessKey, 318 secretKey: credentials.SecretKey, 319 dates: []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)}, 320 policy: `{"3/aws4_request"]]}`, 321 }, 322 } 323 324 for i, testCase := range testCasesV4BadData { 325 // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler. 326 rec := httptest.NewRecorder() 327 328 testCase.policy = fmt.Sprintf(testCase.policy, testCase.dates...) 329 330 req, perr := newPostRequestV4Generic("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, 331 testCase.secretKey, region, curTime, []byte(testCase.policy), nil, testCase.corruptedBase64, testCase.corruptedMultipart) 332 if perr != nil { 333 t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", i+1, instanceType, perr) 334 } 335 // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler. 336 // Call the ServeHTTP to execute the handler. 337 apiRouter.ServeHTTP(rec, req) 338 if rec.Code != testCase.expectedRespStatus { 339 t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code) 340 } 341 } 342 343 testCases2 := []struct { 344 objectName string 345 data []byte 346 expectedRespStatus int 347 accessKey string 348 secretKey string 349 malformedBody bool 350 ignoreContentLength bool 351 }{ 352 // Success case. 353 { 354 objectName: "test", 355 data: bytes.Repeat([]byte("a"), 1025), 356 expectedRespStatus: http.StatusNoContent, 357 accessKey: credentials.AccessKey, 358 secretKey: credentials.SecretKey, 359 malformedBody: false, 360 ignoreContentLength: false, 361 }, 362 // Failed with Content-Length not specified. 363 { 364 objectName: "test", 365 data: bytes.Repeat([]byte("a"), 1025), 366 expectedRespStatus: http.StatusNoContent, 367 accessKey: credentials.AccessKey, 368 secretKey: credentials.SecretKey, 369 malformedBody: false, 370 ignoreContentLength: true, 371 }, 372 // Failed with entity too small. 373 { 374 objectName: "test", 375 data: bytes.Repeat([]byte("a"), 1023), 376 expectedRespStatus: http.StatusBadRequest, 377 accessKey: credentials.AccessKey, 378 secretKey: credentials.SecretKey, 379 malformedBody: false, 380 ignoreContentLength: false, 381 }, 382 // Failed with entity too large. 383 { 384 objectName: "test", 385 data: bytes.Repeat([]byte("a"), (1*humanize.MiByte)+1), 386 expectedRespStatus: http.StatusBadRequest, 387 accessKey: credentials.AccessKey, 388 secretKey: credentials.SecretKey, 389 malformedBody: false, 390 ignoreContentLength: false, 391 }, 392 } 393 394 for i, testCase := range testCases2 { 395 // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler. 396 rec := httptest.NewRecorder() 397 var req *http.Request 398 var perr error 399 if testCase.ignoreContentLength { 400 req, perr = newPostRequestV4("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, testCase.secretKey) 401 } else { 402 req, perr = newPostRequestV4WithContentLength("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, testCase.secretKey) 403 } 404 if perr != nil { 405 t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", i+1, instanceType, perr) 406 } 407 // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler. 408 // Call the ServeHTTP to execute the handler. 409 apiRouter.ServeHTTP(rec, req) 410 if rec.Code != testCase.expectedRespStatus { 411 t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code) 412 } 413 } 414 415 } 416 417 // Wrapper for calling TestPostPolicyBucketHandlerRedirect tests for both Erasure multiple disks and single node setup. 418 func TestPostPolicyBucketHandlerRedirect(t *testing.T) { 419 ExecObjectLayerTest(t, testPostPolicyBucketHandlerRedirect) 420 } 421 422 // testPostPolicyBucketHandlerRedirect tests POST Object when success_action_redirect is specified 423 func testPostPolicyBucketHandlerRedirect(obj ObjectLayer, instanceType string, t TestErrHandler) { 424 if err := newTestConfig(globalMinioDefaultRegion, obj); err != nil { 425 t.Fatalf("Initializing config.json failed") 426 } 427 428 // get random bucket name. 429 bucketName := getRandomBucketName() 430 431 // Key specified in Form data 432 keyName := "test/object" 433 434 var opts ObjectOptions 435 436 // The final name of the upload object 437 targetObj := keyName + "/upload.txt" 438 439 // The url of success_action_redirect field 440 redirectURL, err := url.Parse("http://www.google.com") 441 if err != nil { 442 t.Fatal(err) 443 } 444 445 // Register the API end points with Erasure/FS object layer. 446 apiRouter := initTestAPIEndPoints(obj, []string{"PostPolicy"}) 447 448 credentials := globalActiveCred 449 450 curTime := UTCNow() 451 curTimePlus5Min := curTime.Add(time.Minute * 5) 452 453 err = obj.MakeBucketWithLocation(context.Background(), bucketName, BucketOptions{}) 454 if err != nil { 455 // Failed to create newbucket, abort. 456 t.Fatalf("%s : %s", instanceType, err.Error()) 457 } 458 459 // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler. 460 rec := httptest.NewRecorder() 461 462 dates := []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)} 463 policy := `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], {"success_action_redirect":"` + redirectURL.String() + `"},["starts-with", "$key", "test/"], ["eq", "$x-amz-meta-uuid", "1234"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"]]}` 464 465 // Generate the final policy document 466 policy = fmt.Sprintf(policy, dates...) 467 468 region := "us-east-1" 469 // Create a new POST request with success_action_redirect field specified 470 req, perr := newPostRequestV4Generic("", bucketName, keyName, []byte("objData"), 471 credentials.AccessKey, credentials.SecretKey, region, curTime, 472 []byte(policy), map[string]string{"success_action_redirect": redirectURL.String()}, false, false) 473 474 if perr != nil { 475 t.Fatalf("%s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", instanceType, perr) 476 } 477 // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler. 478 // Call the ServeHTTP to execute the handler. 479 apiRouter.ServeHTTP(rec, req) 480 481 // Check the status code, which must be 303 because success_action_redirect is specified 482 if rec.Code != http.StatusSeeOther { 483 t.Errorf("%s: Expected the response status to be `%d`, but instead found `%d`", instanceType, http.StatusSeeOther, rec.Code) 484 } 485 486 // Get the uploaded object info 487 info, err := obj.GetObjectInfo(context.Background(), bucketName, targetObj, opts) 488 if err != nil { 489 t.Error("Unexpected error: ", err) 490 } 491 492 redirectURL.RawQuery = getRedirectPostRawQuery(info) 493 expectedLocation := redirectURL.String() 494 495 // Check the new location url 496 if rec.Header().Get("Location") != expectedLocation { 497 t.Errorf("Unexpected location, expected = %s, found = `%s`", rec.Header().Get("Location"), expectedLocation) 498 } 499 500 } 501 502 // postPresignSignatureV4 - presigned signature for PostPolicy requests. 503 func postPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, location string) string { 504 // Get signining key. 505 signingkey := getSigningKey(secretAccessKey, t, location, "s3") 506 // Calculate signature. 507 signature := getSignature(signingkey, policyBase64) 508 return signature 509 } 510 511 func newPostRequestV2(endPoint, bucketName, objectName string, accessKey, secretKey string) (*http.Request, error) { 512 // Expire the request five minutes from now. 513 expirationTime := UTCNow().Add(time.Minute * 5) 514 // Create a new post policy. 515 policy := newPostPolicyBytesV2(bucketName, objectName, expirationTime) 516 // Only need the encoding. 517 encodedPolicy := base64.StdEncoding.EncodeToString(policy) 518 519 // Presign with V4 signature based on the policy. 520 signature := calculateSignatureV2(encodedPolicy, secretKey) 521 522 formData := map[string]string{ 523 "AWSAccessKeyId": accessKey, 524 "bucket": bucketName, 525 "key": objectName + "/${filename}", 526 "policy": encodedPolicy, 527 "signature": signature, 528 } 529 530 // Create the multipart form. 531 var buf bytes.Buffer 532 w := multipart.NewWriter(&buf) 533 534 // Set the normal formData 535 for k, v := range formData { 536 w.WriteField(k, v) 537 } 538 // Set the File formData 539 writer, err := w.CreateFormFile("file", "upload.txt") 540 if err != nil { 541 // return nil, err 542 return nil, err 543 } 544 writer.Write([]byte("hello world")) 545 // Close before creating the new request. 546 w.Close() 547 548 // Set the body equal to the created policy. 549 reader := bytes.NewReader(buf.Bytes()) 550 551 req, err := http.NewRequest(http.MethodPost, makeTestTargetURL(endPoint, bucketName, "", nil), reader) 552 if err != nil { 553 return nil, err 554 } 555 556 // Set form content-type. 557 req.Header.Set("Content-Type", w.FormDataContentType()) 558 return req, nil 559 } 560 561 func buildGenericPolicy(t time.Time, accessKey, region, bucketName, objectName string, contentLengthRange bool) []byte { 562 // Expire the request five minutes from now. 563 expirationTime := t.Add(time.Minute * 5) 564 565 credStr := getCredentialString(accessKey, region, t) 566 // Create a new post policy. 567 policy := newPostPolicyBytesV4(credStr, bucketName, objectName, expirationTime) 568 if contentLengthRange { 569 policy = newPostPolicyBytesV4WithContentRange(credStr, bucketName, objectName, expirationTime) 570 } 571 return policy 572 } 573 574 func newPostRequestV4Generic(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string, region string, 575 t time.Time, policy []byte, addFormData map[string]string, corruptedB64 bool, corruptedMultipart bool) (*http.Request, error) { 576 // Get the user credential. 577 credStr := getCredentialString(accessKey, region, t) 578 579 // Only need the encoding. 580 encodedPolicy := base64.StdEncoding.EncodeToString(policy) 581 582 if corruptedB64 { 583 encodedPolicy = "%!~&" + encodedPolicy 584 } 585 586 // Presign with V4 signature based on the policy. 587 signature := postPresignSignatureV4(encodedPolicy, t, secretKey, region) 588 589 formData := map[string]string{ 590 "bucket": bucketName, 591 "key": objectName + "/${filename}", 592 "x-amz-credential": credStr, 593 "policy": encodedPolicy, 594 "x-amz-signature": signature, 595 "x-amz-date": t.Format(iso8601DateFormat), 596 "x-amz-algorithm": "AWS4-HMAC-SHA256", 597 "x-amz-meta-uuid": "1234", 598 "Content-Encoding": "gzip", 599 } 600 601 // Add form data 602 for k, v := range addFormData { 603 formData[k] = v 604 } 605 606 // Create the multipart form. 607 var buf bytes.Buffer 608 w := multipart.NewWriter(&buf) 609 610 // Set the normal formData 611 for k, v := range formData { 612 w.WriteField(k, v) 613 } 614 // Set the File formData but don't if we want send an incomplete multipart request 615 if !corruptedMultipart { 616 writer, err := w.CreateFormFile("file", "upload.txt") 617 if err != nil { 618 // return nil, err 619 return nil, err 620 } 621 writer.Write(objData) 622 // Close before creating the new request. 623 w.Close() 624 } 625 626 // Set the body equal to the created policy. 627 reader := bytes.NewReader(buf.Bytes()) 628 629 req, err := http.NewRequest(http.MethodPost, makeTestTargetURL(endPoint, bucketName, "", nil), reader) 630 if err != nil { 631 return nil, err 632 } 633 634 // Set form content-type. 635 req.Header.Set("Content-Type", w.FormDataContentType()) 636 return req, nil 637 } 638 639 func newPostRequestV4WithContentLength(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) { 640 t := UTCNow() 641 region := "us-east-1" 642 policy := buildGenericPolicy(t, accessKey, region, bucketName, objectName, true) 643 return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, region, t, policy, nil, false, false) 644 } 645 646 func newPostRequestV4(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) { 647 t := UTCNow() 648 region := "us-east-1" 649 policy := buildGenericPolicy(t, accessKey, region, bucketName, objectName, false) 650 return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, region, t, policy, nil, false, false) 651 }