package s3api import ( "bytes" "encoding/base64" "errors" "fmt" "io" "mime/multipart" "net/http" "net/url" "strings" "github.com/chrislusf/seaweedfs/weed/glog" "github.com/chrislusf/seaweedfs/weed/s3api/policy" "github.com/chrislusf/seaweedfs/weed/s3api/s3err" "github.com/dustin/go-humanize" "github.com/gorilla/mux" ) func (s3a *S3ApiServer) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) { // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html bucket := mux.Vars(r)["bucket"] glog.V(3).Infof("PostPolicyBucketHandler %s", bucket) reader, err := r.MultipartReader() if err != nil { s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPOSTRequest) return } form, err := reader.ReadForm(int64(5 * humanize.MiByte)) if err != nil { s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPOSTRequest) return } defer form.RemoveAll() fileBody, fileName, fileSize, formValues, err := extractPostPolicyFormValues(form) if err != nil { s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPOSTRequest) return } if fileBody == nil { s3err.WriteErrorResponse(w, r, s3err.ErrPOSTFileRequired) return } defer fileBody.Close() formValues.Set("Bucket", bucket) if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") { formValues.Set("Key", strings.Replace(formValues.Get("Key"), "${filename}", fileName, -1)) } object := formValues.Get("Key") successRedirect := formValues.Get("success_action_redirect") successStatus := formValues.Get("success_action_status") var redirectURL *url.URL if successRedirect != "" { redirectURL, err = url.Parse(successRedirect) if err != nil { s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPOSTRequest) return } } // Verify policy signature. errCode := s3a.iam.doesPolicySignatureMatch(formValues) if errCode != s3err.ErrNone { s3err.WriteErrorResponse(w, r, errCode) return } policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy")) if err != nil { s3err.WriteErrorResponse(w, r, s3err.ErrMalformedPOSTRequest) return } // Handle policy if it is set. if len(policyBytes) > 0 { postPolicyForm, err := policy.ParsePostPolicyForm(string(policyBytes)) if err != nil { s3err.WriteErrorResponse(w, r, s3err.ErrPostPolicyConditionInvalidFormat) return } // Make sure formValues adhere to policy restrictions. if err = policy.CheckPostPolicy(formValues, postPolicyForm); err != nil { w.Header().Set("Location", r.URL.Path) w.WriteHeader(http.StatusTemporaryRedirect) return } // Ensure that the object size is within expected range, also the file size // should not exceed the maximum single Put size (5 GiB) lengthRange := postPolicyForm.Conditions.ContentLengthRange if lengthRange.Valid { if fileSize < lengthRange.Min { s3err.WriteErrorResponse(w, r, s3err.ErrEntityTooSmall) return } if fileSize > lengthRange.Max { s3err.WriteErrorResponse(w, r, s3err.ErrEntityTooLarge) return } } } uploadUrl := fmt.Sprintf("http://%s%s/%s%s", s3a.option.Filer.ToHttpAddress(), s3a.option.BucketsPath, bucket, urlPathEscape(object)) etag, errCode := s3a.putToFiler(r, uploadUrl, fileBody) if errCode != s3err.ErrNone { s3err.WriteErrorResponse(w, r, errCode) return } if successRedirect != "" { // Replace raw query params.. redirectURL.RawQuery = getRedirectPostRawQuery(bucket, object, etag) w.Header().Set("Location", redirectURL.String()) s3err.WriteEmptyResponse(w, r, http.StatusSeeOther) return } setEtag(w, etag) // Decide what http response to send depending on success_action_status parameter switch successStatus { case "201": resp := PostResponse{ Bucket: bucket, Key: object, ETag: `"` + etag + `"`, Location: w.Header().Get("Location"), } s3err.WriteXMLResponse(w, r, http.StatusCreated, resp) case "200": s3err.WriteEmptyResponse(w, r, http.StatusOK) default: writeSuccessResponseEmpty(w, r) } } // Extract form fields and file data from a HTTP POST Policy func extractPostPolicyFormValues(form *multipart.Form) (filePart io.ReadCloser, fileName string, fileSize int64, formValues http.Header, err error) { // / HTML Form values fileName = "" // Canonicalize the form values into http.Header. formValues = make(http.Header) for k, v := range form.Value { formValues[http.CanonicalHeaderKey(k)] = v } // Validate form values. if err = validateFormFieldSize(formValues); err != nil { return nil, "", 0, nil, err } // this means that filename="" was not specified for file key and Go has // an ugly way of handling this situation. Refer here // https://golang.org/src/mime/multipart/formdata.go#L61 if len(form.File) == 0 { var b = &bytes.Buffer{} for _, v := range formValues["File"] { b.WriteString(v) } fileSize = int64(b.Len()) filePart = io.NopCloser(b) return filePart, fileName, fileSize, formValues, nil } // Iterator until we find a valid File field and break for k, v := range form.File { canonicalFormName := http.CanonicalHeaderKey(k) if canonicalFormName == "File" { if len(v) == 0 { return nil, "", 0, nil, errors.New("Invalid arguments specified") } // Fetch fileHeader which has the uploaded file information fileHeader := v[0] // Set filename fileName = fileHeader.Filename // Open the uploaded part filePart, err = fileHeader.Open() if err != nil { return nil, "", 0, nil, err } // Compute file size fileSize, err = filePart.(io.Seeker).Seek(0, 2) if err != nil { return nil, "", 0, nil, err } // Reset Seek to the beginning _, err = filePart.(io.Seeker).Seek(0, 0) if err != nil { return nil, "", 0, nil, err } // File found and ready for reading break } } return filePart, fileName, fileSize, formValues, nil } // Validate form field size for s3 specification requirement. func validateFormFieldSize(formValues http.Header) error { // Iterate over form values for k := range formValues { // Check if value's field exceeds S3 limit if int64(len(formValues.Get(k))) > int64(1*humanize.MiByte) { return errors.New("Data size larger than expected") } } // Success. return nil } func getRedirectPostRawQuery(bucket, key, etag string) string { redirectValues := make(url.Values) redirectValues.Set("bucket", bucket) redirectValues.Set("key", key) redirectValues.Set("etag", "\""+etag+"\"") return redirectValues.Encode() } // Check to see if Policy is signed correctly. func (iam *IdentityAccessManagement) doesPolicySignatureMatch(formValues http.Header) s3err.ErrorCode { // For SignV2 - Signature field will be valid if _, ok := formValues["Signature"]; ok { return iam.doesPolicySignatureV2Match(formValues) } return iam.doesPolicySignatureV4Match(formValues) }