package s3api import ( "bytes" "encoding/base64" "errors" "fmt" "io" "mime/multipart" "net/http" "net/url" "strings" "github.com/dustin/go-humanize" "github.com/gorilla/mux" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/s3api/policy" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" ) 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, fileContentType, 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)) } rawObject := formValues.Get("Key") if rawObject == "" || !s3_constants.IsValidObjectKey(rawObject) { s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest) return } object := s3_constants.NormalizeObjectKey(rawObject) if err := s3a.validateTableBucketObjectPath(bucket, object); err != nil { s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) return } 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. identity, errCode := s3a.iam.doesPolicySignatureMatch(formValues) if errCode != s3err.ErrNone { s3err.WriteErrorResponse(w, r, errCode) return } if identity != nil { r = r.WithContext(s3_constants.SetIdentityNameInContext(r.Context(), identity.Name)) } 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 { glog.V(3).Infof("PostPolicy check failed for bucket %s: %v", bucket, err) s3err.WriteErrorResponseWithMessage(w, r, s3err.ErrAccessDenied, err.Error()) 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 } } } filePath := fmt.Sprintf("%s/%s", s3a.bucketDir(bucket), object) // Get ContentType from post formData // Otherwise from formFile ContentType contentType := formValues.Get("Content-Type") if contentType == "" { contentType = fileContentType } r.Header.Set("Content-Type", contentType) // Forward validated POST form fields to the underlying PUT as headers. applyPostPolicyFormHeaders(r, formValues) // Authorize the object like the PUT path; the coarse Write check above does not consult the bucket policy. if errCode := s3a.iam.AuthorizeObjectWrite(r, identity, bucket, object); errCode != s3err.ErrNone { s3err.WriteErrorResponse(w, r, errCode) return } // Use fileSize, not r.ContentLength: the multipart body wrapping form // fields and boundaries inflates ContentLength relative to the // object body, which would mis-evaluate any size-filtered rule. ttlSec := s3a.lifecycleTTLForObjectWrite(bucket, object, fileSize) etag, errCode, sseMetadata := s3a.putToFiler(r, filePath, fileBody, bucket, object, 1, ttlSec, nil, false, "") 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) // Include SSE response headers (important for bucket-default encryption) s3a.setSSEResponseHeaders(w, r, sseMetadata) // 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) s3err.PostLog(r, http.StatusCreated, s3err.ErrNone) case "200": s3err.WriteEmptyResponse(w, r, http.StatusOK) case "204": s3err.WriteEmptyResponse(w, r, http.StatusNoContent) default: s3err.WriteEmptyResponse(w, r, http.StatusNoContent) } } // postPolicyReservedFormFields are multipart form fields that are part of the // POST Object auth/policy mechanism (or handled explicitly elsewhere in the // handler) and must not be forwarded to the upload as HTTP headers. Keys are // already run through http.CanonicalHeaderKey before lookup. var postPolicyReservedFormFields = map[string]struct{}{ // POST policy signature (V2) "Policy": {}, "Signature": {}, "Awsaccesskeyid": {}, // POST policy signature (V4) "X-Amz-Signature": {}, "X-Amz-Credential": {}, "X-Amz-Algorithm": {}, "X-Amz-Date": {}, "X-Amz-Security-Token": {}, // Target descriptors "Key": {}, "File": {}, "Bucket": {}, // Success actions (handled elsewhere in the handler) "Success_action_redirect": {}, "Success_action_status": {}, "Redirect": {}, // Content-Type is resolved separately above (from form or file part) "Content-Type": {}, } // applyPostPolicyFormHeaders forwards validated POST Object form fields to the // request headers so they are applied to the resulting PUT. Reserved fields // that are part of the POST policy mechanism itself (signature, key, etc.) are // skipped. The acl form field is translated to the X-Amz-Acl header to match // how AWS promotes the form value to the underlying PUT. func applyPostPolicyFormHeaders(r *http.Request, formValues http.Header) { for k := range formValues { if _, reserved := postPolicyReservedFormFields[k]; reserved { continue } switch { case k == "Acl": r.Header.Set(s3_constants.AmzCannedAcl, formValues.Get(k)) case k == "Cache-Control", k == "Expires", k == "Content-Disposition", k == "Content-Encoding", k == "Content-Language": r.Header.Set(k, formValues.Get(k)) case strings.HasPrefix(k, "X-Amz-"): r.Header.Set(k, formValues.Get(k)) } } } // Extract form fields and file data from a HTTP POST Policy func extractPostPolicyFormValues(form *multipart.Form) (filePart io.ReadCloser, fileName, fileContentType string, fileSize int64, formValues http.Header, err error) { // / HTML Form values fileName = "" fileContentType = "" // 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, fileContentType, 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 // Set contentType fileContentType = fileHeader.Header.Get("Content-Type") // 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, fileContentType, 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, returning the signing identity. func (iam *IdentityAccessManagement) doesPolicySignatureMatch(formValues http.Header) (*Identity, s3err.ErrorCode) { // For SignV2 - Signature field will be valid if _, ok := formValues["Signature"]; ok { return iam.doesPolicySignatureV2Match(formValues) } return iam.doesPolicySignatureV4Match(formValues) }