You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							271 lines
						
					
					
						
							7.7 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							271 lines
						
					
					
						
							7.7 KiB
						
					
					
				
								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))
							 | 
						|
									}
							 | 
						|
									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, urlEscapeObject(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)
							 | 
						|
								
							 | 
						|
									// Add s3 postpolicy support header
							 | 
						|
									for k, _ := range formValues {
							 | 
						|
										if k == "Cache-Control" || k == "Expires" || k == "Content-Disposition" {
							 | 
						|
											r.Header.Set(k, formValues.Get(k))
							 | 
						|
											continue
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if strings.HasPrefix(k, s3_constants.AmzUserMetaPrefix) {
							 | 
						|
											r.Header.Set(k, formValues.Get(k))
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									etag, errCode, _ := s3a.putToFiler(r, uploadUrl, fileBody, "", bucket, 1)
							 | 
						|
								
							 | 
						|
									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)
							 | 
						|
										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)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// 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.
							 | 
						|
								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)
							 | 
						|
								}
							 |