package api import ( "context" "net/http" "sync" "github.com/TrueCloudLab/frostfs-s3-gw/api/auth" "github.com/TrueCloudLab/frostfs-s3-gw/api/data" "github.com/google/uuid" "github.com/gorilla/mux" "go.uber.org/zap" "google.golang.org/grpc/metadata" ) type ( // Handler is an S3 API handler interface. Handler interface { HeadObjectHandler(http.ResponseWriter, *http.Request) GetObjectACLHandler(http.ResponseWriter, *http.Request) PutObjectACLHandler(http.ResponseWriter, *http.Request) GetObjectTaggingHandler(http.ResponseWriter, *http.Request) PutObjectTaggingHandler(http.ResponseWriter, *http.Request) DeleteObjectTaggingHandler(http.ResponseWriter, *http.Request) SelectObjectContentHandler(http.ResponseWriter, *http.Request) GetObjectRetentionHandler(http.ResponseWriter, *http.Request) GetObjectLegalHoldHandler(http.ResponseWriter, *http.Request) GetObjectHandler(http.ResponseWriter, *http.Request) GetObjectAttributesHandler(http.ResponseWriter, *http.Request) CopyObjectHandler(http.ResponseWriter, *http.Request) PutObjectRetentionHandler(http.ResponseWriter, *http.Request) PutObjectLegalHoldHandler(http.ResponseWriter, *http.Request) PutObjectHandler(http.ResponseWriter, *http.Request) DeleteObjectHandler(http.ResponseWriter, *http.Request) GetBucketLocationHandler(http.ResponseWriter, *http.Request) GetBucketPolicyHandler(http.ResponseWriter, *http.Request) GetBucketLifecycleHandler(http.ResponseWriter, *http.Request) GetBucketEncryptionHandler(http.ResponseWriter, *http.Request) GetBucketACLHandler(http.ResponseWriter, *http.Request) PutBucketACLHandler(http.ResponseWriter, *http.Request) GetBucketCorsHandler(http.ResponseWriter, *http.Request) PutBucketCorsHandler(http.ResponseWriter, *http.Request) DeleteBucketCorsHandler(http.ResponseWriter, *http.Request) GetBucketWebsiteHandler(http.ResponseWriter, *http.Request) GetBucketAccelerateHandler(http.ResponseWriter, *http.Request) GetBucketRequestPaymentHandler(http.ResponseWriter, *http.Request) GetBucketLoggingHandler(http.ResponseWriter, *http.Request) GetBucketReplicationHandler(http.ResponseWriter, *http.Request) GetBucketTaggingHandler(http.ResponseWriter, *http.Request) DeleteBucketWebsiteHandler(http.ResponseWriter, *http.Request) DeleteBucketTaggingHandler(http.ResponseWriter, *http.Request) GetBucketObjectLockConfigHandler(http.ResponseWriter, *http.Request) GetBucketVersioningHandler(http.ResponseWriter, *http.Request) GetBucketNotificationHandler(http.ResponseWriter, *http.Request) ListenBucketNotificationHandler(http.ResponseWriter, *http.Request) ListObjectsV2MHandler(http.ResponseWriter, *http.Request) ListObjectsV2Handler(http.ResponseWriter, *http.Request) ListBucketObjectVersionsHandler(http.ResponseWriter, *http.Request) ListObjectsV1Handler(http.ResponseWriter, *http.Request) PutBucketLifecycleHandler(http.ResponseWriter, *http.Request) PutBucketEncryptionHandler(http.ResponseWriter, *http.Request) PutBucketPolicyHandler(http.ResponseWriter, *http.Request) PutBucketObjectLockConfigHandler(http.ResponseWriter, *http.Request) PutBucketTaggingHandler(http.ResponseWriter, *http.Request) PutBucketVersioningHandler(http.ResponseWriter, *http.Request) PutBucketNotificationHandler(http.ResponseWriter, *http.Request) CreateBucketHandler(http.ResponseWriter, *http.Request) HeadBucketHandler(http.ResponseWriter, *http.Request) PostObject(http.ResponseWriter, *http.Request) DeleteMultipleObjectsHandler(http.ResponseWriter, *http.Request) DeleteBucketPolicyHandler(http.ResponseWriter, *http.Request) DeleteBucketLifecycleHandler(http.ResponseWriter, *http.Request) DeleteBucketEncryptionHandler(http.ResponseWriter, *http.Request) DeleteBucketHandler(http.ResponseWriter, *http.Request) ListBucketsHandler(http.ResponseWriter, *http.Request) Preflight(w http.ResponseWriter, r *http.Request) AppendCORSHeaders(w http.ResponseWriter, r *http.Request) CreateMultipartUploadHandler(http.ResponseWriter, *http.Request) UploadPartHandler(http.ResponseWriter, *http.Request) UploadPartCopy(w http.ResponseWriter, r *http.Request) CompleteMultipartUploadHandler(http.ResponseWriter, *http.Request) AbortMultipartUploadHandler(http.ResponseWriter, *http.Request) ListPartsHandler(w http.ResponseWriter, r *http.Request) ListMultipartUploadsHandler(http.ResponseWriter, *http.Request) ResolveBucket(ctx context.Context, bucket string) (*data.BucketInfo, error) } // mimeType represents various MIME types used in API responses. mimeType string logResponseWriter struct { sync.Once http.ResponseWriter statusCode int } ) const ( // SlashSeparator -- slash separator. SlashSeparator = "/" // MimeNone means no response type. MimeNone mimeType = "" // MimeXML means response type is XML. MimeXML mimeType = "application/xml" ) var _ = logErrorResponse func (lrw *logResponseWriter) WriteHeader(code int) { lrw.Do(func() { lrw.statusCode = code lrw.ResponseWriter.WriteHeader(code) }) } func setRequestID(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // generate random UUIDv4 id, _ := uuid.NewRandom() // set request id into response header w.Header().Set(hdrAmzRequestID, id.String()) // set request id into gRPC meta header r = r.WithContext(metadata.AppendToOutgoingContext( r.Context(), hdrAmzRequestID, id.String(), )) // set request info into context r = r.WithContext(prepareContext(w, r)) // continue execution h.ServeHTTP(w, r) }) } func appendCORS(handler Handler) mux.MiddlewareFunc { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler.AppendCORSHeaders(w, r) h.ServeHTTP(w, r) }) } } func resolveBucket(log *zap.Logger, resolveBucket func(ctx context.Context, bucket string) (*data.BucketInfo, error)) mux.MiddlewareFunc { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reqInfo := GetReqInfo(r.Context()) if reqInfo.BucketName != "" && reqInfo.API != "CreateBucket" { bktInfo, err := resolveBucket(r.Context(), reqInfo.BucketName) if err != nil { code := WriteErrorResponse(w, reqInfo, err) log.Error("failed to resolve bucket", zap.Int("status", code), zap.String("request_id", reqInfo.RequestID), zap.String("method", reqInfo.API), zap.String("bucket", reqInfo.BucketName), zap.String("object", reqInfo.ObjectName), zap.Error(err)) return } // todo: (@KirillovDenis) consider save bktInfo into ReqInfo // (in order to optimize resolving bucket in further handlers) r = r.WithContext(SetCID(r.Context(), bktInfo.CID)) } h.ServeHTTP(w, r) }) } } func logErrorResponse(l *zap.Logger) mux.MiddlewareFunc { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { lw := &logResponseWriter{ResponseWriter: w} reqInfo := GetReqInfo(r.Context()) // pass execution: h.ServeHTTP(lw, r) // Ignore >400 status codes if lw.statusCode >= http.StatusBadRequest { return } l.Info("call method", zap.Int("status", lw.statusCode), zap.String("host", r.Host), zap.String("request_id", GetRequestID(r.Context())), zap.String("method", mux.CurrentRoute(r).GetName()), zap.String("bucket", reqInfo.BucketName), zap.String("object", reqInfo.ObjectName), zap.String("description", http.StatusText(lw.statusCode))) }) } } // GetRequestID returns the request ID from the response writer or the context. func GetRequestID(v interface{}) string { switch t := v.(type) { case context.Context: return GetReqInfo(t).RequestID case http.ResponseWriter: return t.Header().Get(hdrAmzRequestID) default: panic("unknown type") } } // Attach adds S3 API handlers from h to r for domains with m client limit using // center authentication and log logger. func Attach(r *mux.Router, domains []string, m MaxClients, h Handler, center auth.Center, log *zap.Logger) { api := r.PathPrefix(SlashSeparator).Subrouter() api.Use( // -- prepare request setRequestID, // -- resolve bucket to set cid in context resolveBucket(log, h.ResolveBucket), // -- logging error requests logErrorResponse(log), ) // Attach user authentication for all S3 routes. AttachUserAuth(api, center, log) buckets := make([]*mux.Router, 0, len(domains)+1) buckets = append(buckets, api.PathPrefix("/{bucket}").Subrouter()) for _, domain := range domains { buckets = append(buckets, api.Host("{bucket:.+}."+domain).Subrouter()) } for _, bucket := range buckets { // Object operations // HeadObject bucket.Use( // -- append CORS headers to a response for appendCORS(h), ) bucket.Methods(http.MethodOptions).HandlerFunc(m.Handle(APIStats("preflight", h.Preflight))).Name("Options") bucket.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("headobject", h.HeadObjectHandler))).Name("HeadObject") // CopyObjectPart bucket.Methods(http.MethodPut).Path("/{object:.+}").Headers(hdrAmzCopySource, "").HandlerFunc(m.Handle(APIStats("uploadpartcopy", h.UploadPartCopy))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}"). Name("UploadPartCopy") // PutObjectPart bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("uploadpart", h.UploadPartHandler))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}"). Name("UploadPart") // ListParts bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("listobjectparts", h.ListPartsHandler))).Queries("uploadId", "{uploadId:.*}"). Name("ListObjectParts") // CompleteMultipartUpload bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("completemutipartupload", h.CompleteMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}"). Name("CompleteMultipartUpload") // CreateMultipartUpload bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("createmultipartupload", h.CreateMultipartUploadHandler))).Queries("uploads", ""). Name("CreateMultipartUpload") // AbortMultipartUpload bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("abortmultipartupload", h.AbortMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}"). Name("AbortMultipartUpload") // ListMultipartUploads bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("listmultipartuploads", h.ListMultipartUploadsHandler))).Queries("uploads", ""). Name("ListMultipartUploads") // GetObjectACL -- this is a dummy call. bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("getobjectacl", h.GetObjectACLHandler))).Queries("acl", ""). Name("GetObjectACL") // PutObjectACL -- this is a dummy call. bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("putobjectacl", h.PutObjectACLHandler))).Queries("acl", ""). Name("PutObjectACL") // GetObjectTagging bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("getobjecttagging", h.GetObjectTaggingHandler))).Queries("tagging", ""). Name("GetObjectTagging") // PutObjectTagging bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("putobjecttagging", h.PutObjectTaggingHandler))).Queries("tagging", ""). Name("PutObjectTagging") // DeleteObjectTagging bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("deleteobjecttagging", h.DeleteObjectTaggingHandler))).Queries("tagging", ""). Name("DeleteObjectTagging") // SelectObjectContent bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("selectobjectcontent", h.SelectObjectContentHandler))).Queries("select", "").Queries("select-type", "2"). Name("SelectObjectContent") // GetObjectRetention bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("getobjectretention", h.GetObjectRetentionHandler))).Queries("retention", ""). Name("GetObjectRetention") // GetObjectLegalHold bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("getobjectlegalhold", h.GetObjectLegalHoldHandler))).Queries("legal-hold", ""). Name("GetObjectLegalHold") // GetObjectAttributes bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("getobjectattributes", h.GetObjectAttributesHandler))).Queries("attributes", ""). Name("GetObjectAttributes") // GetObject bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("getobject", h.GetObjectHandler))). Name("GetObject") // CopyObject bucket.Methods(http.MethodPut).Path("/{object:.+}").Headers(hdrAmzCopySource, "").HandlerFunc(m.Handle(APIStats("copyobject", h.CopyObjectHandler))). Name("CopyObject") // PutObjectRetention bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("putobjectretention", h.PutObjectRetentionHandler))).Queries("retention", ""). Name("PutObjectRetention") // PutObjectLegalHold bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("putobjectlegalhold", h.PutObjectLegalHoldHandler))).Queries("legal-hold", ""). Name("PutObjectLegalHold") // PutObject bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("putobject", h.PutObjectHandler))). Name("PutObject") // DeleteObject bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc( m.Handle(APIStats("deleteobject", h.DeleteObjectHandler))). Name("DeleteObject") // Bucket operations // GetBucketLocation bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketlocation", h.GetBucketLocationHandler))).Queries("location", ""). Name("GetBucketLocation") // GetBucketPolicy bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketpolicy", h.GetBucketPolicyHandler))).Queries("policy", ""). Name("GetBucketPolicy") // GetBucketLifecycle bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketlifecycle", h.GetBucketLifecycleHandler))).Queries("lifecycle", ""). Name("GetBucketLifecycle") // GetBucketEncryption bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketencryption", h.GetBucketEncryptionHandler))).Queries("encryption", ""). Name("GetBucketEncryption") bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketcors", h.GetBucketCorsHandler))).Queries("cors", ""). Name("GetBucketCors") bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(APIStats("putbucketcors", h.PutBucketCorsHandler))).Queries("cors", ""). Name("PutBucketCors") bucket.Methods(http.MethodDelete).HandlerFunc( m.Handle(APIStats("deletebucketcors", h.DeleteBucketCorsHandler))).Queries("cors", ""). Name("DeleteBucketCors") // Dummy Bucket Calls // GetBucketACL -- this is a dummy call. bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketacl", h.GetBucketACLHandler))).Queries("acl", ""). Name("GetBucketACL") // PutBucketACL -- this is a dummy call. bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(APIStats("putbucketacl", h.PutBucketACLHandler))).Queries("acl", ""). Name("PutBucketACL") // GetBucketWebsiteHandler -- this is a dummy call. bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketwebsite", h.GetBucketWebsiteHandler))).Queries("website", ""). Name("GetBucketWebsite") // GetBucketAccelerateHandler -- this is a dummy call. bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketaccelerate", h.GetBucketAccelerateHandler))).Queries("accelerate", ""). Name("GetBucketAccelerate") // GetBucketRequestPaymentHandler -- this is a dummy call. bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketrequestpayment", h.GetBucketRequestPaymentHandler))).Queries("requestPayment", ""). Name("GetBucketRequestPayment") // GetBucketLoggingHandler -- this is a dummy call. bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketlogging", h.GetBucketLoggingHandler))).Queries("logging", ""). Name("GetBucketLogging") // GetBucketLifecycleHandler -- this is a dummy call. bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketlifecycle", h.GetBucketLifecycleHandler))).Queries("lifecycle", ""). Name("GetBucketLifecycle") // GetBucketReplicationHandler -- this is a dummy call. bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketreplication", h.GetBucketReplicationHandler))).Queries("replication", ""). Name("GetBucketReplication") // GetBucketTaggingHandler bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbuckettagging", h.GetBucketTaggingHandler))).Queries("tagging", ""). Name("GetBucketTagging") // DeleteBucketWebsiteHandler bucket.Methods(http.MethodDelete).HandlerFunc( m.Handle(APIStats("deletebucketwebsite", h.DeleteBucketWebsiteHandler))).Queries("website", ""). Name("DeleteBucketWebsite") // DeleteBucketTaggingHandler bucket.Methods(http.MethodDelete).HandlerFunc( m.Handle(APIStats("deletebuckettagging", h.DeleteBucketTaggingHandler))).Queries("tagging", ""). Name("DeleteBucketTagging") // GetBucketObjectLockConfig bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketobjectlockconfiguration", h.GetBucketObjectLockConfigHandler))).Queries("object-lock", ""). Name("GetBucketObjectLockConfig") // GetBucketVersioning bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketversioning", h.GetBucketVersioningHandler))).Queries("versioning", ""). Name("GetBucketVersioning") // GetBucketNotification bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("getbucketnotification", h.GetBucketNotificationHandler))).Queries("notification", ""). Name("GetBucketNotification") // ListenBucketNotification bucket.Methods(http.MethodGet).HandlerFunc(APIStats("listenbucketnotification", h.ListenBucketNotificationHandler)).Queries("events", "{events:.*}"). Name("ListenBucketNotification") // ListObjectsV2M bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("listobjectsv2M", h.ListObjectsV2MHandler))).Queries("list-type", "2", "metadata", "true"). Name("ListObjectsV2M") // ListObjectsV2 bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("listobjectsv2", h.ListObjectsV2Handler))).Queries("list-type", "2"). Name("ListObjectsV2") // ListBucketVersions bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("listbucketversions", h.ListBucketObjectVersionsHandler))).Queries("versions", ""). Name("ListBucketVersions") // ListObjectsV1 (Legacy) bucket.Methods(http.MethodGet).HandlerFunc( m.Handle(APIStats("listobjectsv1", h.ListObjectsV1Handler))). Name("ListObjectsV1") // PutBucketLifecycle bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(APIStats("putbucketlifecycle", h.PutBucketLifecycleHandler))).Queries("lifecycle", ""). Name("PutBucketLifecycle") // PutBucketEncryption bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(APIStats("putbucketencryption", h.PutBucketEncryptionHandler))).Queries("encryption", ""). Name("PutBucketEncryption") // PutBucketPolicy bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(APIStats("putbucketpolicy", h.PutBucketPolicyHandler))).Queries("policy", ""). Name("PutBucketPolicy") // PutBucketObjectLockConfig bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(APIStats("putbucketobjectlockconfig", h.PutBucketObjectLockConfigHandler))).Queries("object-lock", ""). Name("PutBucketObjectLockConfig") // PutBucketTaggingHandler bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(APIStats("putbuckettagging", h.PutBucketTaggingHandler))).Queries("tagging", ""). Name("PutBucketTagging") // PutBucketVersioning bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(APIStats("putbucketversioning", h.PutBucketVersioningHandler))).Queries("versioning", ""). Name("PutBucketVersioning") // PutBucketNotification bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(APIStats("putbucketnotification", h.PutBucketNotificationHandler))).Queries("notification", ""). Name("PutBucketNotification") // CreateBucket bucket.Methods(http.MethodPut).HandlerFunc( m.Handle(APIStats("createbucket", h.CreateBucketHandler))). Name("CreateBucket") // HeadBucket bucket.Methods(http.MethodHead).HandlerFunc( m.Handle(APIStats("headbucket", h.HeadBucketHandler))). Name("HeadBucket") // PostPolicy bucket.Methods(http.MethodPost).HeadersRegexp(hdrContentType, "multipart/form-data*").HandlerFunc( m.Handle(APIStats("postobject", h.PostObject))). Name("PostObject") // DeleteMultipleObjects bucket.Methods(http.MethodPost).HandlerFunc( m.Handle(APIStats("deletemultipleobjects", h.DeleteMultipleObjectsHandler))).Queries("delete", ""). Name("DeleteMultipleObjects") // DeleteBucketPolicy bucket.Methods(http.MethodDelete).HandlerFunc( m.Handle(APIStats("deletebucketpolicy", h.DeleteBucketPolicyHandler))).Queries("policy", ""). Name("DeleteBucketPolicy") // DeleteBucketLifecycle bucket.Methods(http.MethodDelete).HandlerFunc( m.Handle(APIStats("deletebucketlifecycle", h.DeleteBucketLifecycleHandler))).Queries("lifecycle", ""). Name("DeleteBucketLifecycle") // DeleteBucketEncryption bucket.Methods(http.MethodDelete).HandlerFunc( m.Handle(APIStats("deletebucketencryption", h.DeleteBucketEncryptionHandler))).Queries("encryption", ""). Name("DeleteBucketEncryption") // DeleteBucket bucket.Methods(http.MethodDelete).HandlerFunc( m.Handle(APIStats("deletebucket", h.DeleteBucketHandler))). Name("DeleteBucket") } // Root operation // ListBuckets api.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc( m.Handle(APIStats("listbuckets", h.ListBucketsHandler))). Name("ListBuckets") // S3 browser with signature v4 adds '//' for ListBuckets request, so rather // than failing with UnknownAPIRequest we simply handle it for now. api.Methods(http.MethodGet).Path(SlashSeparator + SlashSeparator).HandlerFunc( m.Handle(APIStats("listbuckets", h.ListBucketsHandler))). Name("ListBuckets") // If none of the routes match, add default error handler routes api.NotFoundHandler = APIStats("notfound", errorResponseHandler) api.MethodNotAllowedHandler = APIStats("methodnotallowed", errorResponseHandler) }