From d8e4d3c9c88010a6805894c1d79da92b9a022bde Mon Sep 17 00:00:00 2001 From: Anis Elleuch Date: Tue, 20 Dec 2016 18:32:17 +0100 Subject: [PATCH] POSTForm: Return http 303 if redirect is specified (#3479) success_action_redirect in the sent Form means that the server needs to return 303 in addition to a well specific redirection url, this commit adds this feature --- cmd/api-response.go | 12 +++-- cmd/bucket-handlers.go | 43 ++++++++++++----- cmd/post-policy_test.go | 103 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 137 insertions(+), 21 deletions(-) diff --git a/cmd/api-response.go b/cmd/api-response.go index 120132bb8..420e7a608 100644 --- a/cmd/api-response.go +++ b/cmd/api-response.go @@ -492,17 +492,23 @@ func writeResponse(w http.ResponseWriter, statusCode int, response []byte) { w.(http.Flusher).Flush() } -// writeSuccessResponse write success headers and response if any. +// writeSuccessResponse writes success headers and response if any. func writeSuccessResponse(w http.ResponseWriter, response []byte) { writeResponse(w, http.StatusOK, response) } -// writeSuccessNoContent write success headers with http status 204 +// writeSuccessNoContent writes success headers with http status 204 func writeSuccessNoContent(w http.ResponseWriter) { writeResponse(w, http.StatusNoContent, nil) } -// writeErrorRespone write error headers +// writeRedirectSeeOther writes Location header with http status 303 +func writeRedirectSeeOther(w http.ResponseWriter, location string) { + w.Header().Set("Location", location) + writeResponse(w, http.StatusSeeOther, nil) +} + +// writeErrorRespone writes error headers func writeErrorResponse(w http.ResponseWriter, req *http.Request, errorCode APIErrorCode, resource string) { apiError := getAPIError(errorCode) // set common headers diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index ede9c9643..7a9e12b4b 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -19,6 +19,7 @@ package cmd import ( "encoding/base64" "encoding/xml" + "fmt" "io" "net/http" "net/url" @@ -447,21 +448,37 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"") w.Header().Set("Location", getObjectLocation(bucket, object)) - // Decide what http response to send depending on success_action_status parameter - switch formValues[http.CanonicalHeaderKey("success_action_status")] { - case "201": - resp := encodeResponse(PostResponse{ - Bucket: bucket, - Key: object, - ETag: "\"" + objInfo.MD5Sum + "\"", - Location: getObjectLocation(bucket, object), - }) - writeResponse(w, http.StatusCreated, resp) + successRedirect := formValues[http.CanonicalHeaderKey("success_action_redirect")] + successStatus := formValues[http.CanonicalHeaderKey("success_action_status")] - case "200": - writeSuccessResponse(w, nil) - default: + if successStatus == "" && successRedirect == "" { writeSuccessNoContent(w) + } else { + if successRedirect != "" { + redirectURL := successRedirect + "?" + fmt.Sprintf("bucket=%s&key=%s&etag=%s", + bucket, + getURLEncodedName(object), + getURLEncodedName("\""+objInfo.MD5Sum+"\"")) + + writeRedirectSeeOther(w, redirectURL) + } else { + // Decide what http response to send depending on success_action_status parameter + switch successStatus { + case "201": + resp := encodeResponse(PostResponse{ + Bucket: bucket, + Key: object, + ETag: "\"" + objInfo.MD5Sum + "\"", + Location: getObjectLocation(bucket, object), + }) + writeResponse(w, http.StatusCreated, resp) + + case "200": + writeSuccessResponse(w, nil) + default: + writeSuccessNoContent(w) + } + } } // Notify object created event. diff --git a/cmd/post-policy_test.go b/cmd/post-policy_test.go index 2453d224c..6881f18af 100644 --- a/cmd/post-policy_test.go +++ b/cmd/post-policy_test.go @@ -326,10 +326,10 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler. rec := httptest.NewRecorder() - // policy := buildGenericPolicy(curTime, testCase.accessKey, bucketName, testCase.objectName, false) testCase.policy = fmt.Sprintf(testCase.policy, testCase.dates...) + req, perr := newPostRequestV4Generic("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, - testCase.secretKey, curTime, []byte(testCase.policy), testCase.corruptedBase64, testCase.corruptedMultipart) + testCase.secretKey, curTime, []byte(testCase.policy), nil, testCase.corruptedBase64, testCase.corruptedMultipart) if perr != nil { t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: %v", i+1, instanceType, perr) } @@ -395,6 +395,93 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr } +// Wrapper for calling TestPostPolicyBucketHandlerRedirect tests for both XL multiple disks and single node setup. +func TestPostPolicyBucketHandlerRedirect(t *testing.T) { + ExecObjectLayerTest(t, testPostPolicyBucketHandlerRedirect) +} + +// testPostPolicyBucketHandlerRedirect tests POST Object when success_action_redirect is specified +func testPostPolicyBucketHandlerRedirect(obj ObjectLayer, instanceType string, t TestErrHandler) { + root, err := newTestConfig("us-east-1") + if err != nil { + t.Fatalf("Initializing config.json failed") + } + defer removeAll(root) + + // Register event notifier. + err = initEventNotifier(obj) + if err != nil { + t.Fatalf("Initializing event notifiers failed") + } + + // get random bucket name. + bucketName := getRandomBucketName() + + // Key specified in Form data + keyName := "test/object" + + // The final name of the upload object + targetObj := keyName + "/upload.txt" + + // The url of success_action_redirect field + redirectURL := "http://www.google.com" + + // Register the API end points with XL/FS object layer. + apiRouter := initTestAPIEndPoints(obj, []string{"PostPolicy"}) + + credentials := serverConfig.GetCredential() + + curTime := time.Now().UTC() + curTimePlus5Min := curTime.Add(time.Minute * 5) + + err = obj.MakeBucket(bucketName) + if err != nil { + // Failed to create newbucket, abort. + t.Fatalf("%s : %s", instanceType, err.Error()) + } + + // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler. + rec := httptest.NewRecorder() + + dates := []interface{}{curTimePlus5Min.Format(expirationDateFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)} + policy := `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], {"success_action_redirect":"` + redirectURL + `"},["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKeyID + `/%s/us-east-1/s3/aws4_request"]]}` + + // Generate the final policy document + policy = fmt.Sprintf(policy, dates...) + + // Create a new POST request with success_action_redirect field specified + req, perr := newPostRequestV4Generic("", bucketName, keyName, []byte("objData"), + credentials.AccessKeyID, credentials.SecretAccessKey, curTime, + []byte(policy), map[string]string{"success_action_redirect": redirectURL}, false, false) + + if perr != nil { + t.Fatalf("%s: Failed to create HTTP request for PostPolicyHandler: %v", 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) + + // Check the status code, which must be 303 because success_action_redirect is specified + if rec.Code != http.StatusSeeOther { + t.Errorf("%s: Expected the response status to be `%d`, but instead found `%d`", instanceType, http.StatusSeeOther, rec.Code) + } + + // Get the uploaded object info + info, err := obj.GetObjectInfo(bucketName, targetObj) + if err != nil { + t.Error("Unexpected error: ", err) + } + + expectedLocation := fmt.Sprintf(redirectURL+"?bucket=%s&key=%s&etag=%s", + bucketName, getURLEncodedName(targetObj), getURLEncodedName("\""+info.MD5Sum+"\"")) + + // Check the new location url + if rec.HeaderMap.Get("Location") != expectedLocation { + t.Errorf("Unexpected location, expected = %s, found = `%s`", rec.HeaderMap.Get("Location"), expectedLocation) + } + +} + // postPresignSignatureV4 - presigned signature for PostPolicy requests. func postPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, location string) string { // Get signining key. @@ -467,7 +554,8 @@ func buildGenericPolicy(t time.Time, accessKey, bucketName, objectName string, c return policy } -func newPostRequestV4Generic(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string, t time.Time, policy []byte, corruptedB64 bool, corruptedMultipart bool) (*http.Request, error) { +func newPostRequestV4Generic(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string, + t time.Time, policy []byte, addFormData map[string]string, corruptedB64 bool, corruptedMultipart bool) (*http.Request, error) { // Get the user credential. credStr := getCredential(accessKey, serverConfig.GetRegion(), t) @@ -493,6 +581,11 @@ func newPostRequestV4Generic(endPoint, bucketName, objectName string, objData [] "Content-Encoding": "gzip", } + // Add form data + for k, v := range addFormData { + formData[k] = v + } + // Create the multipart form. var buf bytes.Buffer w := multipart.NewWriter(&buf) @@ -529,11 +622,11 @@ func newPostRequestV4Generic(endPoint, bucketName, objectName string, objData [] func newPostRequestV4WithContentLength(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) { t := time.Now().UTC() policy := buildGenericPolicy(t, accessKey, bucketName, objectName, true) - return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, t, policy, false, false) + return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, t, policy, nil, false, false) } func newPostRequestV4(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) { t := time.Now().UTC() policy := buildGenericPolicy(t, accessKey, bucketName, objectName, false) - return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, t, policy, false, false) + return newPostRequestV4Generic(endPoint, bucketName, objectName, objData, accessKey, secretKey, t, policy, nil, false, false) }