diff --git a/api/auth/center.go b/api/auth/center.go index c77ffe9..78fc3d8 100644 --- a/api/auth/center.go +++ b/api/auth/center.go @@ -2,9 +2,13 @@ package auth import ( "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "io" + "mime/multipart" "net/http" "regexp" "strings" @@ -24,6 +28,9 @@ import ( // authorizationFieldRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter. var authorizationFieldRegexp = regexp.MustCompile(`AWS4-HMAC-SHA256 Credential=(?P[^/]+)/(?P[^/]+)/(?P[^/]*)/(?P[^/]+)/aws4_request,\s*SignedHeaders=(?P.+),\s*Signature=(?P.+)`) +// postPolicyCredentialRegexp -- is regexp for credentials when uploading file using POST with policy. +var postPolicyCredentialRegexp = regexp.MustCompile(`(?P[^/]+)/(?P[^/]+)/(?P[^/]*)/(?P[^/]+)/aws4_request`) + type ( // Center is a user authentication interface. Center interface { @@ -31,8 +38,9 @@ type ( } center struct { - reg *regexpSubmatcher - cli tokens.Credentials + reg *regexpSubmatcher + postReg *regexpSubmatcher + cli tokens.Credentials } // Params stores node connection parameters. @@ -56,6 +64,7 @@ type ( const ( accessKeyPartsNum = 2 authHeaderPartsNum = 6 + maxFormSizeMemory = 50 * 1048576 // 50 MB ) // ErrNoAuthorizationHeader is returned for unauthenticated requests. @@ -74,8 +83,9 @@ var _ io.ReadSeeker = prs(0) // New creates an instance of AuthCenter. func New(conns pool.Pool, key *keys.PrivateKey) Center { return ¢er{ - cli: tokens.New(conns, key), - reg: ®expSubmatcher{re: authorizationFieldRegexp}, + cli: tokens.New(conns, key), + reg: ®expSubmatcher{re: authorizationFieldRegexp}, + postReg: ®expSubmatcher{re: postPolicyCredentialRegexp}, } } @@ -118,6 +128,9 @@ func (c *center) Authenticate(r *http.Request) (*accessbox.Box, error) { authHeaderField := r.Header["Authorization"] if len(authHeaderField) != 1 { + if strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data") { + return c.checkFormData(r) + } return nil, ErrNoAuthorizationHeader } @@ -149,6 +162,51 @@ func (c *center) Authenticate(r *http.Request) (*accessbox.Box, error) { return box, nil } +func (c *center) checkFormData(r *http.Request) (*accessbox.Box, error) { + if err := r.ParseMultipartForm(maxFormSizeMemory); err != nil { + return nil, apiErrors.GetAPIError(apiErrors.ErrInvalidArgument) + } + + if err := prepareForm(r.MultipartForm); err != nil { + return nil, fmt.Errorf("couldn't parse form: %w", err) + } + + policy := MultipartFormValue(r, "policy") + if policy == "" { + return nil, ErrNoAuthorizationHeader + } + + submatches := c.postReg.getSubmatches(MultipartFormValue(r, "x-amz-credential")) + if len(submatches) != 4 { + return nil, apiErrors.GetAPIError(apiErrors.ErrAuthorizationHeaderMalformed) + } + + signatureDateTime, err := time.Parse("20060102T150405Z", MultipartFormValue(r, "x-amz-date")) + if err != nil { + return nil, fmt.Errorf("failed to parse x-amz-date field: %w", err) + } + + address := object.NewAddress() + if err = address.Parse(strings.ReplaceAll(submatches["access_key_id"], "0", "/")); err != nil { + return nil, apiErrors.GetAPIError(apiErrors.ErrInvalidAccessKeyID) + } + + box, err := c.cli.GetBox(r.Context(), address) + if err != nil { + return nil, err + } + + secret := box.Gate.AccessKey + service, region := submatches["service"], submatches["region"] + + signature := signStr(secret, service, region, signatureDateTime, policy) + if signature != MultipartFormValue(r, "x-amz-signature") { + return nil, apiErrors.GetAPIError(apiErrors.ErrSignatureDoesNotMatch) + } + + return box, nil +} + func cloneRequest(r *http.Request, authHeader *authHeader) *http.Request { otherRequest := r.Clone(context.TODO()) otherRequest.Header = make(http.Header) @@ -181,3 +239,77 @@ func (c *center) checkSign(authHeader *authHeader, box *accessbox.Box, request * return nil } + +func signStr(secret, service, region string, t time.Time, strToSign string) string { + creds := deriveKey(secret, service, region, t) + signature := hmacSHA256(creds, []byte(strToSign)) + return hex.EncodeToString(signature) +} + +func deriveKey(secret, service, region string, t time.Time) []byte { + hmacDate := hmacSHA256([]byte("AWS4"+secret), []byte(t.UTC().Format("20060102"))) + hmacRegion := hmacSHA256(hmacDate, []byte(region)) + hmacService := hmacSHA256(hmacRegion, []byte(service)) + return hmacSHA256(hmacService, []byte("aws4_request")) +} + +func hmacSHA256(key []byte, data []byte) []byte { + hash := hmac.New(sha256.New, key) + hash.Write(data) + return hash.Sum(nil) +} + +// MultipartFormValue get value by key from multipart form. +func MultipartFormValue(r *http.Request, key string) string { + if r.MultipartForm == nil { + return "" + } + if vs := r.MultipartForm.Value[key]; len(vs) > 0 { + return vs[0] + } + + return "" +} + +func prepareForm(form *multipart.Form) error { + var oldKeysValue []string + var oldKeysFile []string + + for k, v := range form.Value { + lowerKey := strings.ToLower(k) + if lowerKey != k { + form.Value[lowerKey] = v + oldKeysValue = append(oldKeysValue, k) + } + } + for _, k := range oldKeysValue { + delete(form.Value, k) + } + + for k, v := range form.File { + lowerKey := strings.ToLower(k) + if lowerKey != "file" { + oldKeysFile = append(oldKeysFile, k) + if len(v) > 0 { + field, err := v[0].Open() + if err != nil { + return err + } + + data, err := io.ReadAll(field) + if err != nil { + return err + } + form.Value[lowerKey] = []string{string(data)} + } + } else if lowerKey != k { + form.File[lowerKey] = v + oldKeysFile = append(oldKeysFile, k) + } + } + for _, k := range oldKeysFile { + delete(form.File, k) + } + + return nil +} diff --git a/api/auth/center_test.go b/api/auth/center_test.go index 3e7481d..8318b5b 100644 --- a/api/auth/center_test.go +++ b/api/auth/center_test.go @@ -3,6 +3,7 @@ package auth import ( "strings" "testing" + "time" "github.com/nspcc-dev/neofs-s3-gw/api/errors" "github.com/stretchr/testify/require" @@ -85,3 +86,16 @@ func TestAuthHeaderGetAddress(t *testing.T) { require.Equal(t, tc.err, err, tc.authHeader.AccessKeyID) } } + +func TestSignature(t *testing.T) { + secret := "66be461c3cd429941c55daf42fad2b8153e5a2016ba89c9494d97677cc9d3872" + strToSign := "eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLAogICJjb25kaXRpb25zIjogWwogICAgeyJidWNrZXQiOiAiYWNsIn0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwKICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL2xvY2FsaG9zdDo4MDg0L2FjbCJ9LAogICAgWyJzdGFydHMtd2l0aCIsICIkQ29udGVudC1UeXBlIiwgImltYWdlLyJdLAogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwKICAgIFsic3RhcnRzLXdpdGgiLCAiJHgtYW16LW1ldGEtdGFnIiwgIiJdLAoKICAgIHsiWC1BbXotQ3JlZGVudGlhbCI6ICI4Vmk0MVBIbjVGMXNzY2J4OUhqMXdmMUU2aERUYURpNndxOGhxTU05NllKdTA1QzVDeUVkVlFoV1E2aVZGekFpTkxXaTlFc3BiUTE5ZDRuR3pTYnZVZm10TS8yMDE1MTIyOS91cy1lYXN0LTEvczMvYXdzNF9yZXF1ZXN0In0sCiAgICB7IngtYW16LWFsZ29yaXRobSI6ICJBV1M0LUhNQUMtU0hBMjU2In0sCiAgICB7IlgtQW16LURhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfSwKICAgIHsieC1pZ25vcmUtdG1wIjogInNvbWV0aGluZyIgfQogIF0KfQ==" + + signTime, err := time.Parse("20060102T150405Z", "20151229T000000Z") + if err != nil { + panic(err) + } + + signature := signStr(secret, "s3", "us-east-1", signTime, strToSign) + require.Equal(t, "dfbe886241d9e369cf4b329ca0f15eb27306c97aa1022cc0bb5a914c4ef87634", signature) +} diff --git a/api/handler/put.go b/api/handler/put.go index fa460a1..8c81d51 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -1,19 +1,159 @@ package handler import ( + "bytes" + "encoding/base64" + "encoding/json" "encoding/xml" + "fmt" + "io" "net" "net/http" "net/url" + "strconv" "strings" + "time" "github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl" "github.com/nspcc-dev/neofs-s3-gw/api" + "github.com/nspcc-dev/neofs-s3-gw/api/auth" "github.com/nspcc-dev/neofs-s3-gw/api/errors" "github.com/nspcc-dev/neofs-s3-gw/api/layer" "go.uber.org/zap" ) +type postPolicy struct { + Expiration time.Time `json:"expiration"` + Conditions []*policyCondition `json:"conditions"` + empty bool +} + +func (p *postPolicy) condition(key string) *policyCondition { + for _, condition := range p.Conditions { + if condition.Key == key { + return condition + } + } + return nil +} + +func (p *postPolicy) CheckContentLength(size int64) bool { + if p.empty { + return true + } + for _, condition := range p.Conditions { + if condition.Matching == "content-length-range" { + length := strconv.FormatInt(size, 10) + return condition.Key <= length && length <= condition.Value + } + } + return true +} + +func (p *policyCondition) match(value string) bool { + switch p.Matching { + case "eq": + p.Matched = p.Value == value + case "starts-with": + if p.Key == api.ContentType { + p.Matched = true + for _, contentType := range strings.Split(value, ",") { + if !strings.HasPrefix(contentType, p.Value) { + p.Matched = false + } + } + } else { + p.Matched = strings.HasPrefix(value, p.Value) + } + } + return p.Matched +} + +func (p *postPolicy) CheckField(key string, value string) error { + if p.empty { + return nil + } + cond := p.condition(key) + if cond == nil { + return errors.GetAPIError(errors.ErrPostPolicyConditionInvalidFormat) + } + + if !cond.match(value) { + return errors.GetAPIError(errors.ErrPostPolicyConditionInvalidFormat) + } + + return nil +} + +func (p *postPolicy) AllConditionMatched() bool { + for _, condition := range p.Conditions { + if !condition.Matched { + return false + } + } + return true +} + +type policyCondition struct { + Matching string + Key string + Value string + Matched bool +} + +var errInvalidCondition = fmt.Errorf("invalid condition") + +func (p *policyCondition) UnmarshalJSON(data []byte) error { + var ( + ok bool + v interface{} + ) + + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + switch v := v.(type) { + case []interface{}: + if len(v) != 3 { + return errInvalidCondition + } + if p.Matching, ok = v[0].(string); !ok { + return errInvalidCondition + } + + if p.Matching == "content-length-range" { + min, ok := v[1].(float64) + max, ok2 := v[2].(float64) + if !ok || !ok2 { + return errInvalidCondition + } + p.Key = strconv.FormatFloat(min, 'f', 0, 32) + p.Value = strconv.FormatFloat(max, 'f', 0, 32) + } else { + key, ok2 := v[1].(string) + p.Value, ok = v[2].(string) + if !ok || !ok2 { + return errInvalidCondition + } + p.Key = strings.ToLower(strings.TrimPrefix(key, "$")) + } + + case map[string]interface{}: + p.Matching = "eq" + for key, val := range v { + p.Key = strings.ToLower(key) + if p.Value, ok = val.(string); !ok { + return errInvalidCondition + } + } + default: + return fmt.Errorf("unknown condition type") + } + + return nil +} + // keywords of predefined basic ACL values. const ( basicACLPrivate = "private" @@ -68,49 +208,10 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { } if containsACLHeaders(r) { - objectACL, err := parseACLHeaders(r) - if err != nil { - h.logAndSendError(w, "could not parse object acl", reqInfo, err) + if newEaclTable, err = h.getNewEAclTable(r, info); err != nil { + h.logAndSendError(w, "could not get new eacl table", reqInfo, err) return } - - resInfo := &resourceInfo{ - Bucket: reqInfo.BucketName, - Object: reqInfo.ObjectName, - Version: info.Version(), - } - - bktPolicy, err := aclToPolicy(objectACL, resInfo) - if err != nil { - h.logAndSendError(w, "could not translate object acl to bucket policy", reqInfo, err) - return - } - - astChild, err := policyToAst(bktPolicy) - if err != nil { - h.logAndSendError(w, "could not translate policy to ast", reqInfo, err) - return - } - - bacl, err := h.obj.GetBucketACL(r.Context(), reqInfo.BucketName) - if err != nil { - h.logAndSendError(w, "could not get bucket eacl", reqInfo, err) - return - } - - parentAst := tableToAst(bacl.EACL, reqInfo.BucketName) - for _, resource := range parentAst.Resources { - if resource.Bucket == bacl.Info.CID.String() { - resource.Bucket = reqInfo.BucketName - } - } - - if resAst, updated := mergeAst(parentAst, astChild); updated { - if newEaclTable, err = astToTable(resAst); err != nil { - h.logAndSendError(w, "could not translate ast to table", reqInfo, err) - return - } - } } if tagSet != nil { @@ -142,11 +243,226 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { api.WriteSuccessResponseHeadersOnly(w) } +func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) { + var ( + newEaclTable *eacl.Table + tagSet map[string]string + reqInfo = api.GetReqInfo(r.Context()) + metadata = make(map[string]string) + ) + + policy, err := checkPostPolicy(r, reqInfo, metadata) + if err != nil { + h.logAndSendError(w, "failed check policy", reqInfo, err) + return + } + + if tagging := auth.MultipartFormValue(r, "tagging"); tagging != "" { + buffer := bytes.NewBufferString(tagging) + tagSet, err = readTagSet(buffer) + if err != nil { + h.logAndSendError(w, "could not read tag set", reqInfo, err) + return + } + } + + var contentReader io.Reader + var size int64 + if content, ok := r.MultipartForm.Value["file"]; ok { + contentReader = bytes.NewBufferString(content[0]) + size = int64(len(content[0])) + } else { + file, head, err := r.FormFile("file") + if err != nil { + h.logAndSendError(w, "could get uploading file", reqInfo, err) + return + } + contentReader = file + size = head.Size + reqInfo.ObjectName = strings.ReplaceAll(reqInfo.ObjectName, "${filename}", head.Filename) + } + if !policy.CheckContentLength(size) { + h.logAndSendError(w, "invalid content-length", reqInfo, errors.GetAPIError(errors.ErrInvalidArgument)) + return + } + + params := &layer.PutObjectParams{ + Bucket: reqInfo.BucketName, + Object: reqInfo.ObjectName, + Reader: contentReader, + Size: size, + Header: metadata, + } + + info, err := h.obj.PutObject(r.Context(), params) + if err != nil { + h.logAndSendError(w, "could not upload object", reqInfo, err) + return + } + + if acl := auth.MultipartFormValue(r, "acl"); acl != "" { + r.Header.Set(api.AmzACL, acl) + r.Header.Set(api.AmzGrantFullControl, "") + r.Header.Set(api.AmzGrantWrite, "") + r.Header.Set(api.AmzGrantRead, "") + + if newEaclTable, err = h.getNewEAclTable(r, info); err != nil { + h.logAndSendError(w, "could not get new eacl table", reqInfo, err) + return + } + } + + if tagSet != nil { + if err = h.obj.PutObjectTagging(r.Context(), &layer.PutTaggingParams{ObjectInfo: info, TagSet: tagSet}); err != nil { + h.logAndSendError(w, "could not upload object tagging", reqInfo, err) + return + } + } + + if newEaclTable != nil { + p := &layer.PutBucketACLParams{ + Name: reqInfo.BucketName, + EACL: newEaclTable, + } + + if err = h.obj.PutBucketACL(r.Context(), p); err != nil { + h.logAndSendError(w, "could not put bucket acl", reqInfo, err) + return + } + } + + if versioning, err := h.obj.GetBucketVersioning(r.Context(), reqInfo.BucketName); err != nil { + h.log.Warn("couldn't get bucket versioning", zap.String("bucket name", reqInfo.BucketName), zap.Error(err)) + } else if versioning.VersioningEnabled { + w.Header().Set(api.AmzVersionID, info.Version()) + } + + if redirectURL := auth.MultipartFormValue(r, "success_action_redirect"); redirectURL != "" { + http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) + return + } + status := http.StatusNoContent + if statusStr := auth.MultipartFormValue(r, "success_action_status"); statusStr != "" { + switch statusStr { + case "200": + status = http.StatusOK + case "201": + status = http.StatusCreated + resp := &PostResponse{ + Bucket: info.Bucket, + Key: info.Name, + ETag: info.HashSum, + } + w.WriteHeader(status) + if _, err = w.Write(api.EncodeResponse(resp)); err != nil { + h.logAndSendError(w, "something went wrong", reqInfo, err) + } + return + } + } + + w.Header().Set(api.ETag, info.HashSum) + w.WriteHeader(status) +} + +func checkPostPolicy(r *http.Request, reqInfo *api.ReqInfo, metadata map[string]string) (*postPolicy, error) { + policy := &postPolicy{} + if policyStr := auth.MultipartFormValue(r, "policy"); policyStr != "" { + policyData, err := base64.StdEncoding.DecodeString(policyStr) + if err != nil { + return nil, fmt.Errorf("could not decode policy: %w", err) + } + if err = json.Unmarshal(policyData, policy); err != nil { + return nil, fmt.Errorf("could not unmarshal policy: %w", err) + } + if policy.Expiration.Before(time.Now()) { + return nil, fmt.Errorf("policy is expired: %w", errors.GetAPIError(errors.ErrInvalidArgument)) + } + } + + for key, v := range r.MultipartForm.Value { + value := v[0] + if key == "file" || key == "policy" || key == "x-amz-signature" || strings.HasPrefix(key, "x-ignore-") { + continue + } + if err := policy.CheckField(key, value); err != nil { + return nil, fmt.Errorf("'%s' form field doesn't match the policy: %w", key, err) + } + + prefix := strings.ToLower(api.MetadataPrefix) + if strings.HasPrefix(key, prefix) { + metadata[strings.TrimPrefix(key, prefix)] = value + } + + if key == "content-type" { + metadata[api.ContentType] = value + } + + if key == "key" { + reqInfo.ObjectName = value + } + } + + for _, cond := range policy.Conditions { + if cond.Key == "bucket" { + if !cond.match(reqInfo.BucketName) { + return nil, errors.GetAPIError(errors.ErrPostPolicyConditionInvalidFormat) + } + } + } + + return policy, nil +} + func containsACLHeaders(r *http.Request) bool { return r.Header.Get(api.AmzACL) != "" || r.Header.Get(api.AmzGrantRead) != "" || r.Header.Get(api.AmzGrantFullControl) != "" || r.Header.Get(api.AmzGrantWrite) != "" } +func (h *handler) getNewEAclTable(r *http.Request, objInfo *layer.ObjectInfo) (*eacl.Table, error) { + var newEaclTable *eacl.Table + objectACL, err := parseACLHeaders(r) + if err != nil { + return nil, fmt.Errorf("could not parse object acl: %w", err) + } + + resInfo := &resourceInfo{ + Bucket: objInfo.Bucket, + Object: objInfo.Name, + Version: objInfo.Version(), + } + + bktPolicy, err := aclToPolicy(objectACL, resInfo) + if err != nil { + return nil, fmt.Errorf("could not translate object acl to bucket policy: %w", err) + } + + astChild, err := policyToAst(bktPolicy) + if err != nil { + return nil, fmt.Errorf("could not translate policy to ast: %w", err) + } + + bacl, err := h.obj.GetBucketACL(r.Context(), objInfo.Bucket) + if err != nil { + return nil, fmt.Errorf("could not get bucket eacl: %w", err) + } + + parentAst := tableToAst(bacl.EACL, objInfo.Bucket) + for _, resource := range parentAst.Resources { + if resource.Bucket == bacl.Info.CID.String() { + resource.Bucket = objInfo.Bucket + } + } + + if resAst, updated := mergeAst(parentAst, astChild); updated { + if newEaclTable, err = astToTable(resAst); err != nil { + return nil, fmt.Errorf("could not translate ast to table: %w", err) + } + } + + return newEaclTable, nil +} + func parseTaggingHeader(header http.Header) (map[string]string, error) { var tagSet map[string]string if tagging := header.Get(api.AmzTagging); len(tagging) > 0 { diff --git a/api/handler/put_test.go b/api/handler/put_test.go index 65ea8a0..4695900 100644 --- a/api/handler/put_test.go +++ b/api/handler/put_test.go @@ -1,7 +1,9 @@ package handler import ( + "encoding/json" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -43,3 +45,44 @@ func TestCheckBucketName(t *testing.T) { } } } + +func TestCustomJSONMarshal(t *testing.T) { + data := []byte(` +{ "expiration": "2015-12-30T12:00:00.000Z", + "conditions": [ + ["content-length-range", 1048576, 10485760], + {"bucket": "bucketName"}, + ["starts-with", "$key", "user/user1/"] + ] +}`) + + parsedTime, err := time.Parse(time.RFC3339, "2015-12-30T12:00:00.000Z") + require.NoError(t, err) + + expectedPolicy := &postPolicy{ + Expiration: parsedTime, + Conditions: []*policyCondition{ + { + Matching: "content-length-range", + Key: "1048576", + Value: "10485760", + }, + { + Matching: "eq", + Key: "bucket", + Value: "bucketName", + }, + { + Matching: "starts-with", + Key: "key", + Value: "user/user1/", + }, + }, + } + + policy := &postPolicy{} + err = json.Unmarshal(data, policy) + require.NoError(t, err) + + require.Equal(t, expectedPolicy, policy) +} diff --git a/api/handler/response.go b/api/handler/response.go index 2649a2f..d50a8ec 100644 --- a/api/handler/response.go +++ b/api/handler/response.go @@ -175,6 +175,13 @@ type Tagging struct { TagSet []Tag `xml:"TagSet>Tag"` } +// PostResponse contains result of posting object. +type PostResponse struct { + Bucket string `xml:"Bucket"` + Key string `xml:"Key"` + ETag string `xml:"Etag"` +} + // Tag is AWS key-value tag. type Tag struct { Key string diff --git a/api/handler/unimplemented.go b/api/handler/unimplemented.go index 4567129..48637e6 100644 --- a/api/handler/unimplemented.go +++ b/api/handler/unimplemented.go @@ -114,7 +114,3 @@ func (h *handler) PutBucketEncryptionHandler(w http.ResponseWriter, r *http.Requ func (h *handler) PutBucketObjectLockConfigHandler(w http.ResponseWriter, r *http.Request) { h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented)) } - -func (h *handler) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) { - h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented)) -} diff --git a/api/router.go b/api/router.go index de7990b..0436e3c 100644 --- a/api/router.go +++ b/api/router.go @@ -70,7 +70,7 @@ type ( PutBucketNotificationHandler(http.ResponseWriter, *http.Request) CreateBucketHandler(http.ResponseWriter, *http.Request) HeadBucketHandler(http.ResponseWriter, *http.Request) - PostPolicyBucketHandler(http.ResponseWriter, *http.Request) + PostObject(http.ResponseWriter, *http.Request) DeleteMultipleObjectsHandler(http.ResponseWriter, *http.Request) DeleteBucketPolicyHandler(http.ResponseWriter, *http.Request) DeleteBucketLifecycleHandler(http.ResponseWriter, *http.Request) @@ -422,8 +422,8 @@ func Attach(r *mux.Router, domains []string, m MaxClients, h Handler, center aut Name("HeadBucket") // PostPolicy bucket.Methods(http.MethodPost).HeadersRegexp(hdrContentType, "multipart/form-data*").HandlerFunc( - m.Handle(metrics.APIStats("postpolicybucket", h.PostPolicyBucketHandler))). - Name("PostPolicyBucket") + m.Handle(metrics.APIStats("postobject", h.PostObject))). + Name("PostObject") // DeleteMultipleObjects bucket.Methods(http.MethodPost).HandlerFunc( m.Handle(metrics.APIStats("deletemultipleobjects", h.DeleteMultipleObjectsHandler))).Queries("delete", "").