aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3api_object_handlers_postpolicy.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/s3api/s3api_object_handlers_postpolicy.go')
-rw-r--r--weed/s3api/s3api_object_handlers_postpolicy.go241
1 files changed, 241 insertions, 0 deletions
diff --git a/weed/s3api/s3api_object_handlers_postpolicy.go b/weed/s3api/s3api_object_handlers_postpolicy.go
new file mode 100644
index 000000000..035302ae6
--- /dev/null
+++ b/weed/s3api/s3api_object_handlers_postpolicy.go
@@ -0,0 +1,241 @@
+package s3api
+
+import (
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "github.com/chrislusf/seaweedfs/weed/s3api/policy"
+ "github.com/chrislusf/seaweedfs/weed/s3api/s3err"
+ "github.com/dustin/go-humanize"
+ "github.com/gorilla/mux"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+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"]
+
+ reader, err := r.MultipartReader()
+ if err != nil {
+ writeErrorResponse(w, s3err.ErrMalformedPOSTRequest, r.URL)
+ return
+ }
+ form, err := reader.ReadForm(int64(5 * humanize.MiByte))
+ if err != nil {
+ writeErrorResponse(w, s3err.ErrMalformedPOSTRequest, r.URL)
+ return
+ }
+ defer form.RemoveAll()
+
+ fileBody, fileName, fileSize, formValues, err := extractPostPolicyFormValues(form)
+ if err != nil {
+ writeErrorResponse(w, s3err.ErrMalformedPOSTRequest, r.URL)
+ return
+ }
+ if fileBody == nil {
+ writeErrorResponse(w, s3err.ErrPOSTFileRequired, r.URL)
+ 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 {
+ writeErrorResponse(w, s3err.ErrMalformedPOSTRequest, r.URL)
+ return
+ }
+ }
+
+ // Verify policy signature.
+ errCode := s3a.iam.doesPolicySignatureMatch(formValues)
+ if errCode != s3err.ErrNone {
+ writeErrorResponse(w, errCode, r.URL)
+ return
+ }
+
+ policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))
+ if err != nil {
+ writeErrorResponse(w, s3err.ErrMalformedPOSTRequest, r.URL)
+ return
+ }
+
+ // Handle policy if it is set.
+ if len(policyBytes) > 0 {
+
+ postPolicyForm, err := policy.ParsePostPolicyForm(string(policyBytes))
+ if err != nil {
+ writeErrorResponse(w, s3err.ErrPostPolicyConditionInvalidFormat, r.URL)
+ 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 {
+ writeErrorResponse(w, s3err.ErrEntityTooSmall, r.URL)
+ return
+ }
+
+ if fileSize > lengthRange.Max {
+ writeErrorResponse(w, s3err.ErrEntityTooLarge, r.URL)
+ return
+ }
+ }
+ }
+
+ uploadUrl := fmt.Sprintf("http://%s%s/%s%s", s3a.option.Filer, s3a.option.BucketsPath, bucket, urlPathEscape(object))
+
+ etag, errCode := s3a.putToFiler(r, uploadUrl, fileBody)
+
+ if errCode != s3err.ErrNone {
+ writeErrorResponse(w, errCode, r.URL)
+ return
+ }
+
+ if successRedirect != "" {
+ // Replace raw query params..
+ redirectURL.RawQuery = getRedirectPostRawQuery(bucket, object, etag)
+ w.Header().Set("Location", redirectURL.String())
+ writeResponse(w, http.StatusSeeOther, nil, mimeNone)
+ return
+ }
+
+ setEtag(w, etag)
+
+ // Decide what http response to send depending on success_action_status parameter
+ switch successStatus {
+ case "201":
+ resp := encodeResponse(PostResponse{
+ Bucket: bucket,
+ Key: object,
+ ETag: `"` + etag + `"`,
+ Location: w.Header().Get("Location"),
+ })
+ writeResponse(w, http.StatusCreated, resp, mimeXML)
+ case "200":
+ writeResponse(w, http.StatusOK, nil, mimeNone)
+ default:
+ writeSuccessResponseEmpty(w)
+ }
+
+}
+
+// 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 = ioutil.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)
+}