mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2024-12-27 19:37:55 +08:00
ffe5f19aa0
* Add s3 postpolicy support header * optimized code
270 lines
7.6 KiB
Go
270 lines
7.6 KiB
Go
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)
|
|
|
|
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)
|
|
default:
|
|
writeSuccessResponseEmpty(w, r)
|
|
}
|
|
|
|
}
|
|
|
|
// 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)
|
|
}
|