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