package api import ( "context" "fmt" "net/http" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" s3middleware "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "go.uber.org/zap" ) 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) } ) func AttachChi(api *chi.Mux, domains []string, throttle middleware.ThrottleOpts, h Handler, center auth.Center, log *zap.Logger, appMetrics *metrics.AppMetrics) { api.Use( middleware.CleanPath, s3middleware.Request(log), middleware.ThrottleWithOpts(throttle), middleware.Recoverer, s3middleware.Tracing(), s3middleware.Metrics(log, h.ResolveBucket, appMetrics), s3middleware.LogSuccessResponse(log), s3middleware.Auth(center, log), ) defaultRouter := chi.NewRouter() defaultRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.BucketURLPrm), bucketRouter(h, log)) defaultRouter.Get("/", named("ListBuckets", h.ListBucketsHandler)) hr := NewHostBucketRouter("bucket") hr.Default(defaultRouter) for _, domain := range domains { hr.Map(domain, bucketRouter(h, log)) } api.Mount("/", hr) attachErrorHandler(api, h, center, log, appMetrics) } func named(name string, handlerFunc http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { reqInfo := s3middleware.GetReqInfo(r.Context()) reqInfo.API = name handlerFunc.ServeHTTP(w, r) } } func setErrorAPI(apiName string, h http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := s3middleware.SetReqInfo(r.Context(), &s3middleware.ReqInfo{API: apiName}) h.ServeHTTP(w, r.WithContext(ctx)) } } // If none of the http routes match respond with appropriate errors. func errorResponseHandler(w http.ResponseWriter, r *http.Request) { desc := fmt.Sprintf("Unknown API request at %s", r.URL.Path) s3middleware.WriteErrorResponse(w, s3middleware.GetReqInfo(r.Context()), errors.Error{ Code: "UnknownAPIRequest", Description: desc, HTTPStatusCode: http.StatusBadRequest, }) } // attachErrorHandler set NotFoundHandler and MethodNotAllowedHandler for chi.Router. func attachErrorHandler(api *chi.Mux, h Handler, center auth.Center, log *zap.Logger, appMetrics *metrics.AppMetrics) { middlewares := chi.Middlewares{ s3middleware.Auth(center, log), s3middleware.Metrics(log, h.ResolveBucket, appMetrics), } var errorHandler http.Handler = http.HandlerFunc(errorResponseHandler) for i := len(middlewares) - 1; i >= 0; i-- { errorHandler = middlewares[i](errorHandler) } // If none of the routes match, add default error handler routes api.NotFound(setErrorAPI("NotFound", errorHandler)) api.MethodNotAllowed(setErrorAPI("MethodNotAllowed", errorHandler)) } func bucketRouter(h Handler, log *zap.Logger) chi.Router { bktRouter := chi.NewRouter() bktRouter.Use( s3middleware.AddBucketName(log), s3middleware.WrapHandler(h.AppendCORSHeaders), ) bktRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.ObjectURLPrm), objectRouter(h, log)) bktRouter.Options("/", h.Preflight) bktRouter.Head("/", named("HeadBucket", h.HeadBucketHandler)) // GET method handlers bktRouter.Group(func(r chi.Router) { r.Method(http.MethodGet, "/", NewHandlerFilter(). Add(NewFilter(). Queries("upload"). Handler(named("ListMultipartUploads", h.ListMultipartUploadsHandler))). Add(NewFilter(). Queries("location"). Handler(named("GetBucketLocation", h.GetBucketLocationHandler))). Add(NewFilter(). Queries("policy"). Handler(named("GetBucketPolicy", h.GetBucketPolicyHandler))). Add(NewFilter(). Queries("lifecycle"). Handler(named("GetBucketLifecycle", h.GetBucketLifecycleHandler))). Add(NewFilter(). Queries("encryption"). Handler(named("GetBucketEncryption", h.GetBucketEncryptionHandler))). Add(NewFilter(). Queries("cors"). Handler(named("GetBucketCors", h.GetBucketCorsHandler))). Add(NewFilter(). Queries("acl"). Handler(named("GetBucketACL", h.GetBucketACLHandler))). Add(NewFilter(). Queries("website"). Handler(named("GetBucketWebsite", h.GetBucketWebsiteHandler))). Add(NewFilter(). Queries("accelerate"). Handler(named("GetBucketAccelerate", h.GetBucketAccelerateHandler))). Add(NewFilter(). Queries("requestPayment"). Handler(named("GetBucketRequestPayment", h.GetBucketRequestPaymentHandler))). Add(NewFilter(). Queries("logging"). Handler(named("GetBucketLogging", h.GetBucketLoggingHandler))). Add(NewFilter(). Queries("replication"). Handler(named("GetBucketReplication", h.GetBucketReplicationHandler))). Add(NewFilter(). Queries("tagging"). Handler(named("GetBucketTagging", h.GetBucketTaggingHandler))). Add(NewFilter(). Queries("object-lock"). Handler(named("GetBucketObjectLockConfig", h.GetBucketObjectLockConfigHandler))). Add(NewFilter(). Queries("versioning"). Handler(named("GetBucketVersioning", h.GetBucketVersioningHandler))). Add(NewFilter(). Queries("notification"). Handler(named("GetBucketNotification", h.GetBucketNotificationHandler))). Add(NewFilter(). Queries("events"). Handler(named("ListenBucketNotification", h.ListenBucketNotificationHandler))). Add(NewFilter(). QueriesMatch("list-type", "2", "metadata", "true"). Handler(named("ListObjectsV2M", h.ListObjectsV2MHandler))). Add(NewFilter(). QueriesMatch("list-type", "2"). Handler(named("ListObjectsV2", h.ListObjectsV2Handler))). Add(NewFilter(). Queries("versions"). Handler(named("ListBucketObjectVersions", h.ListBucketObjectVersionsHandler))). DefaultHandler(named("ListObjectsV1", h.ListObjectsV1Handler))) }) // PUT method handlers bktRouter.Group(func(r chi.Router) { r.Method(http.MethodPut, "/", NewHandlerFilter(). Add(NewFilter(). Queries("cors"). Handler(named("PutBucketCors", h.PutBucketCorsHandler))). Add(NewFilter(). Queries("acl"). Handler(named("PutBucketACL", h.PutBucketACLHandler))). Add(NewFilter(). Queries("lifecycle"). Handler(named("PutBucketLifecycle", h.PutBucketLifecycleHandler))). Add(NewFilter(). Queries("encryption"). Handler(named("PutBucketEncryption", h.PutBucketEncryptionHandler))). Add(NewFilter(). Queries("policy"). Handler(named("PutBucketPolicy", h.PutBucketPolicyHandler))). Add(NewFilter(). Queries("object-lock"). Handler(named("PutBucketObjectLockConfig", h.PutBucketObjectLockConfigHandler))). Add(NewFilter(). Queries("tagging"). Handler(named("PutBucketTagging", h.PutBucketTaggingHandler))). Add(NewFilter(). Queries("versioning"). Handler(named("PutBucketVersioning", h.PutBucketVersioningHandler))). Add(NewFilter(). Queries("notification"). Handler(named("PutBucketNotification", h.PutBucketNotificationHandler))). DefaultHandler(named("CreateBucket", h.CreateBucketHandler))) }) // POST method handlers bktRouter.Group(func(r chi.Router) { r.Method(http.MethodPost, "/", NewHandlerFilter(). Add(NewFilter(). Queries("delete"). Handler(named("DeleteMultipleObjects", h.DeleteMultipleObjectsHandler))). // todo consider add filter to match header for defaultHandler: hdrContentType, "multipart/form-data*" DefaultHandler(named("PostObject", h.PostObject))) }) // DELETE method handlers bktRouter.Group(func(r chi.Router) { r.Method(http.MethodDelete, "/", NewHandlerFilter(). Add(NewFilter(). Queries("cors"). Handler(named("DeleteBucketCors", h.DeleteBucketCorsHandler))). Add(NewFilter(). Queries("website"). Handler(named("DeleteBucketWebsite", h.DeleteBucketWebsiteHandler))). Add(NewFilter(). Queries("tagging"). Handler(named("DeleteBucketTagging", h.DeleteBucketTaggingHandler))). Add(NewFilter(). Queries("policy"). Handler(named("PutBucketPolicy", h.PutBucketPolicyHandler))). Add(NewFilter(). Queries("lifecycle"). Handler(named("PutBucketLifecycle", h.PutBucketLifecycleHandler))). Add(NewFilter(). Queries("encryption"). Handler(named("DeleteBucketEncryption", h.DeleteBucketEncryptionHandler))). DefaultHandler(named("DeleteBucket", h.DeleteBucketHandler))) }) return bktRouter } func objectRouter(h Handler, l *zap.Logger) chi.Router { objRouter := chi.NewRouter() objRouter.Use(s3middleware.AddObjectName(l)) objRouter.Head("/", named("HeadObject", h.HeadObjectHandler)) // GET method handlers objRouter.Group(func(r chi.Router) { r.Method(http.MethodGet, "/", NewHandlerFilter(). Add(NewFilter(). Queries("uploadId"). Handler(named("ListParts", h.ListPartsHandler))). Add(NewFilter(). Queries("acl"). Handler(named("GetObjectACL", h.GetObjectACLHandler))). Add(NewFilter(). Queries("tagging"). Handler(named("GetObjectTagging", h.GetObjectTaggingHandler))). Add(NewFilter(). Queries("retention"). Handler(named("GetObjectRetention", h.GetObjectRetentionHandler))). Add(NewFilter(). Queries("legal-hold"). Handler(named("GetObjectLegalHold", h.GetObjectLegalHoldHandler))). Add(NewFilter(). Queries("attributes"). Handler(named("GetObjectAttributes", h.GetObjectAttributesHandler))). DefaultHandler(named("GetObject", h.GetObjectHandler))) }) // PUT method handlers objRouter.Group(func(r chi.Router) { r.Method(http.MethodPut, "/", NewHandlerFilter(). Add(NewFilter(). Headers(AmzCopySource). Queries("partNumber", "uploadId"). Handler(named("UploadPartCopy", h.UploadPartCopy))). Add(NewFilter(). Queries("partNumber", "uploadId"). Handler(named("UploadPart", h.UploadPartHandler))). Add(NewFilter(). Queries("acl"). Handler(named("PutObjectACL", h.PutObjectACLHandler))). Add(NewFilter(). Queries("tagging"). Handler(named("PutObjectTagging", h.PutObjectTaggingHandler))). Add(NewFilter(). Headers(AmzCopySource). Handler(named("CopyObject", h.CopyObjectHandler))). Add(NewFilter(). Queries("retention"). Handler(named("PutObjectRetention", h.PutObjectRetentionHandler))). Add(NewFilter(). Queries("legal-hold"). Handler(named("PutObjectLegalHold", h.PutObjectLegalHoldHandler))). DefaultHandler(named("PutObject", h.PutObjectHandler))) }) // POST method handlers objRouter.Group(func(r chi.Router) { r.Method(http.MethodPost, "/", NewHandlerFilter(). Add(NewFilter(). Queries("uploadId"). Handler(named("CompleteMultipartUpload", h.CompleteMultipartUploadHandler))). Add(NewFilter(). Queries("uploads"). Handler(named("CreateMultipartUpload", h.CreateMultipartUploadHandler))). DefaultHandler(named("SelectObjectContent", h.SelectObjectContentHandler))) }) // DELETE method handlers objRouter.Group(func(r chi.Router) { r.Method(http.MethodDelete, "/", NewHandlerFilter(). Add(NewFilter(). Queries("uploadId"). Handler(named("AbortMultipartUpload", h.AbortMultipartUploadHandler))). Add(NewFilter(). Queries("tagging"). Handler(named("DeleteObjectTagging", h.DeleteObjectTaggingHandler))). DefaultHandler(named("DeleteObject", h.DeleteObjectHandler))) }) return objRouter }