package api import ( "context" "fmt" "net/http" "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/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "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) GetBucketPolicyStatusHandler(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) PatchObjectHandler(http.ResponseWriter, *http.Request) ResolveBucket(ctx context.Context, bucket string) (*data.BucketInfo, error) ResolveCID(ctx context.Context, bucket string) (cid.ID, error) } ) type Settings interface { s3middleware.RequestSettings s3middleware.PolicySettings s3middleware.MetricsSettings s3middleware.VHSSettings s3middleware.LogHTTPSettings } type FrostFSID interface { s3middleware.FrostFSIDValidator s3middleware.FrostFSIDInformer } type Config struct { Throttle middleware.ThrottleOpts Handler Handler Center s3middleware.Center Log *zap.Logger Metrics *metrics.AppMetrics MiddlewareSettings Settings FrostfsID FrostFSID FrostFSIDValidation bool PolicyChecker engine.ChainRouter XMLDecoder s3middleware.XMLDecoder Tagging s3middleware.ResourceTagging } func NewRouter(cfg Config) *chi.Mux { api := chi.NewRouter() api.Use( s3middleware.LogHTTP(cfg.Log, cfg.MiddlewareSettings), s3middleware.Request(cfg.Log, cfg.MiddlewareSettings), middleware.ThrottleWithOpts(cfg.Throttle), middleware.Recoverer, s3middleware.Metrics(cfg.Log, cfg.Handler.ResolveCID, cfg.Metrics, cfg.MiddlewareSettings), s3middleware.LogSuccessResponse(cfg.Log), s3middleware.Auth(cfg.Center, cfg.Log), ) if cfg.FrostFSIDValidation { api.Use(s3middleware.FrostfsIDValidation(cfg.FrostfsID, cfg.Log)) } api.Use(s3middleware.PrepareAddressStyle(cfg.MiddlewareSettings, cfg.Log)) api.Use(s3middleware.PolicyCheck(s3middleware.PolicyConfig{ Storage: cfg.PolicyChecker, FrostfsID: cfg.FrostfsID, Settings: cfg.MiddlewareSettings, Log: cfg.Log, BucketResolver: cfg.Handler.ResolveBucket, Decoder: cfg.XMLDecoder, Tagging: cfg.Tagging, })) defaultRouter := chi.NewRouter() defaultRouter.Mount("/{bucket}", bucketRouter(cfg.Handler)) defaultRouter.Get("/", named(s3middleware.ListBucketsOperation, cfg.Handler.ListBucketsHandler)) attachErrorHandler(defaultRouter) vhsRouter := bucketRouter(cfg.Handler) router := newGlobalRouter(defaultRouter, vhsRouter) api.Mount("/", router) attachErrorHandler(api) return api } type globalRouter struct { pathStyleRouter chi.Router vhsRouter chi.Router } func newGlobalRouter(pathStyleRouter, vhsRouter chi.Router) *globalRouter { return &globalRouter{ pathStyleRouter: pathStyleRouter, vhsRouter: vhsRouter, } } func (g *globalRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { router := g.pathStyleRouter if reqInfo := s3middleware.GetReqInfo(r.Context()); reqInfo.RequestVHSEnabled { router = g.vhsRouter } router.ServeHTTP(w, r) } 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) } } // If none of the http routes match respond with appropriate errors. func errorResponseHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqInfo := s3middleware.GetReqInfo(ctx) desc := fmt.Sprintf("Unknown API request at %s", r.URL.Path) _, wrErr := s3middleware.WriteErrorResponse(w, reqInfo, errors.Error{ Code: "UnknownAPIRequest", Description: desc, HTTPStatusCode: http.StatusBadRequest, }) if log := s3middleware.GetReqLog(ctx); log != nil { fields := []zap.Field{ zap.String("method", reqInfo.API), zap.String("http method", r.Method), zap.String("url", r.RequestURI), } if wrErr != nil { fields = append(fields, zap.NamedError("write_response_error", wrErr)) } log.Error(logs.RequestUnmatched, fields...) } } func notSupportedHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqInfo := s3middleware.GetReqInfo(ctx) _, wrErr := s3middleware.WriteErrorResponse(w, reqInfo, errors.GetAPIError(errors.ErrNotSupported)) if log := s3middleware.GetReqLog(ctx); log != nil { fields := []zap.Field{ zap.String("http method", r.Method), zap.String("url", r.RequestURI), } if wrErr != nil { fields = append(fields, zap.NamedError("write_response_error", wrErr)) } log.Error(logs.NotSupported, fields...) } } } // attachErrorHandler set NotFoundHandler and MethodNotAllowedHandler for chi.Router. func attachErrorHandler(api *chi.Mux) { errorHandler := http.HandlerFunc(errorResponseHandler) // If none of the routes match, add default error handler routes api.NotFound(named("NotFound", errorHandler)) api.MethodNotAllowed(named("MethodNotAllowed", errorHandler)) } func bucketRouter(h Handler) chi.Router { bktRouter := chi.NewRouter() bktRouter.Use( s3middleware.WrapHandler(h.AppendCORSHeaders), ) bktRouter.Mount("/", objectRouter(h)) bktRouter.Options("/", named(s3middleware.OptionsBucketOperation, h.Preflight)) bktRouter.Head("/", named(s3middleware.HeadBucketOperation, h.HeadBucketHandler)) // GET method handlers bktRouter.Group(func(r chi.Router) { r.Method(http.MethodGet, "/", NewHandlerFilter(). Add(NewFilter(). Queries(s3middleware.UploadsQuery). Handler(named(s3middleware.ListMultipartUploadsOperation, h.ListMultipartUploadsHandler))). Add(NewFilter(). Queries(s3middleware.LocationQuery). Handler(named(s3middleware.GetBucketLocationOperation, h.GetBucketLocationHandler))). Add(NewFilter(). Queries(s3middleware.PolicyStatusQuery). Handler(named(s3middleware.GetBucketPolicyStatusOperation, h.GetBucketPolicyStatusHandler))). Add(NewFilter(). Queries(s3middleware.PolicyQuery). Handler(named(s3middleware.GetBucketPolicyOperation, h.GetBucketPolicyHandler))). Add(NewFilter(). Queries(s3middleware.LifecycleQuery). Handler(named(s3middleware.GetBucketLifecycleOperation, h.GetBucketLifecycleHandler))). Add(NewFilter(). Queries(s3middleware.EncryptionQuery). Handler(named(s3middleware.GetBucketEncryptionOperation, h.GetBucketEncryptionHandler))). Add(NewFilter(). Queries(s3middleware.CorsQuery). Handler(named(s3middleware.GetBucketCorsOperation, h.GetBucketCorsHandler))). Add(NewFilter(). Queries(s3middleware.ACLQuery). Handler(named(s3middleware.GetBucketACLOperation, h.GetBucketACLHandler))). Add(NewFilter(). Queries(s3middleware.WebsiteQuery). Handler(named(s3middleware.GetBucketWebsiteOperation, h.GetBucketWebsiteHandler))). Add(NewFilter(). Queries(s3middleware.AccelerateQuery). Handler(named(s3middleware.GetBucketAccelerateOperation, h.GetBucketAccelerateHandler))). Add(NewFilter(). Queries(s3middleware.RequestPaymentQuery). Handler(named(s3middleware.GetBucketRequestPaymentOperation, h.GetBucketRequestPaymentHandler))). Add(NewFilter(). Queries(s3middleware.LoggingQuery). Handler(named(s3middleware.GetBucketLoggingOperation, h.GetBucketLoggingHandler))). Add(NewFilter(). Queries(s3middleware.ReplicationQuery). Handler(named(s3middleware.GetBucketReplicationOperation, h.GetBucketReplicationHandler))). Add(NewFilter(). Queries(s3middleware.TaggingQuery). Handler(named(s3middleware.GetBucketTaggingOperation, h.GetBucketTaggingHandler))). Add(NewFilter(). Queries(s3middleware.ObjectLockQuery). Handler(named(s3middleware.GetBucketObjectLockConfigOperation, h.GetBucketObjectLockConfigHandler))). Add(NewFilter(). Queries(s3middleware.VersioningQuery). Handler(named(s3middleware.GetBucketVersioningOperation, h.GetBucketVersioningHandler))). Add(NewFilter(). Queries(s3middleware.NotificationQuery). Handler(named(s3middleware.GetBucketNotificationOperation, h.GetBucketNotificationHandler))). Add(NewFilter(). Queries(s3middleware.EventsQuery). Handler(named(s3middleware.ListenBucketNotificationOperation, h.ListenBucketNotificationHandler))). Add(NewFilter(). QueriesMatch(s3middleware.ListTypeQuery, "2", s3middleware.MetadataQuery, "true"). Handler(named(s3middleware.ListObjectsV2MOperation, h.ListObjectsV2MHandler))). Add(NewFilter(). QueriesMatch(s3middleware.ListTypeQuery, "2"). Handler(named(s3middleware.ListObjectsV2Operation, h.ListObjectsV2Handler))). Add(NewFilter(). Queries(s3middleware.VersionsQuery). Handler(named(s3middleware.ListBucketObjectVersionsOperation, h.ListBucketObjectVersionsHandler))). Add(NewFilter(). AllowedQueries(s3middleware.QueryDelimiter, s3middleware.QueryMaxKeys, s3middleware.QueryPrefix, s3middleware.QueryMarker, s3middleware.QueryEncodingType). Handler(named(s3middleware.ListObjectsV1Operation, h.ListObjectsV1Handler))). Add(NewFilter(). NoQueries(). Handler(listWrapper(h))). DefaultHandler(notSupportedHandler())) }) // PUT method handlers bktRouter.Group(func(r chi.Router) { r.Method(http.MethodPut, "/", NewHandlerFilter(). Add(NewFilter(). Queries(s3middleware.CorsQuery). Handler(named(s3middleware.PutBucketCorsOperation, h.PutBucketCorsHandler))). Add(NewFilter(). Queries(s3middleware.ACLQuery). Handler(named(s3middleware.PutBucketACLOperation, h.PutBucketACLHandler))). Add(NewFilter(). Queries(s3middleware.LifecycleQuery). Handler(named(s3middleware.PutBucketLifecycleOperation, h.PutBucketLifecycleHandler))). Add(NewFilter(). Queries(s3middleware.EncryptionQuery). Handler(named(s3middleware.PutBucketEncryptionOperation, h.PutBucketEncryptionHandler))). Add(NewFilter(). Queries(s3middleware.PolicyQuery). Handler(named(s3middleware.PutBucketPolicyOperation, h.PutBucketPolicyHandler))). Add(NewFilter(). Queries(s3middleware.ObjectLockQuery). Handler(named(s3middleware.PutBucketObjectLockConfigOperation, h.PutBucketObjectLockConfigHandler))). Add(NewFilter(). Queries(s3middleware.TaggingQuery). Handler(named(s3middleware.PutBucketTaggingOperation, h.PutBucketTaggingHandler))). Add(NewFilter(). Queries(s3middleware.VersioningQuery). Handler(named(s3middleware.PutBucketVersioningOperation, h.PutBucketVersioningHandler))). Add(NewFilter(). Queries(s3middleware.NotificationQuery). Handler(named(s3middleware.PutBucketNotificationOperation, h.PutBucketNotificationHandler))). Add(NewFilter(). NoQueries(). Handler(named(s3middleware.CreateBucketOperation, h.CreateBucketHandler))). DefaultHandler(notSupportedHandler())) }) // POST method handlers bktRouter.Group(func(r chi.Router) { r.Method(http.MethodPost, "/", NewHandlerFilter(). Add(NewFilter(). Queries(s3middleware.DeleteQuery). Handler(named(s3middleware.DeleteMultipleObjectsOperation, h.DeleteMultipleObjectsHandler))). // todo consider add filter to match header for defaultHandler: hdrContentType, "multipart/form-data*" DefaultHandler(named(s3middleware.PostObjectOperation, h.PostObject))) }) // DELETE method handlers bktRouter.Group(func(r chi.Router) { r.Method(http.MethodDelete, "/", NewHandlerFilter(). Add(NewFilter(). Queries(s3middleware.CorsQuery). Handler(named(s3middleware.DeleteBucketCorsOperation, h.DeleteBucketCorsHandler))). Add(NewFilter(). Queries(s3middleware.WebsiteQuery). Handler(named(s3middleware.DeleteBucketWebsiteOperation, h.DeleteBucketWebsiteHandler))). Add(NewFilter(). Queries(s3middleware.TaggingQuery). Handler(named(s3middleware.DeleteBucketTaggingOperation, h.DeleteBucketTaggingHandler))). Add(NewFilter(). Queries(s3middleware.PolicyQuery). Handler(named(s3middleware.DeleteBucketPolicyOperation, h.DeleteBucketPolicyHandler))). Add(NewFilter(). Queries(s3middleware.LifecycleQuery). Handler(named(s3middleware.DeleteBucketLifecycleOperation, h.DeleteBucketLifecycleHandler))). Add(NewFilter(). Queries(s3middleware.EncryptionQuery). Handler(named(s3middleware.DeleteBucketEncryptionOperation, h.DeleteBucketEncryptionHandler))). Add(NewFilter(). NoQueries(). Handler(named(s3middleware.DeleteBucketOperation, h.DeleteBucketHandler))). DefaultHandler(notSupportedHandler())) }) attachErrorHandler(bktRouter) return bktRouter } func listWrapper(h Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if reqInfo := s3middleware.GetReqInfo(r.Context()); reqInfo.BucketName == "" { reqInfo.API = s3middleware.ListBucketsOperation h.ListBucketsHandler(w, r) } else { reqInfo.API = s3middleware.ListObjectsV1Operation h.ListObjectsV1Handler(w, r) } } } func objectRouter(h Handler) chi.Router { objRouter := chi.NewRouter() objRouter.Options("/*", named(s3middleware.OptionsObjectOperation, h.Preflight)) objRouter.Head("/*", named(s3middleware.HeadObjectOperation, h.HeadObjectHandler)) objRouter.Patch("/*", named(s3middleware.PatchObjectOperation, h.PatchObjectHandler)) // GET method handlers objRouter.Group(func(r chi.Router) { r.Method(http.MethodGet, "/*", NewHandlerFilter(). Add(NewFilter(). Queries(s3middleware.UploadIDQuery). Handler(named(s3middleware.ListPartsOperation, h.ListPartsHandler))). Add(NewFilter(). Queries(s3middleware.ACLQuery). Handler(named(s3middleware.GetObjectACLOperation, h.GetObjectACLHandler))). Add(NewFilter(). Queries(s3middleware.TaggingQuery). Handler(named(s3middleware.GetObjectTaggingOperation, h.GetObjectTaggingHandler))). Add(NewFilter(). Queries(s3middleware.RetentionQuery). Handler(named(s3middleware.GetObjectRetentionOperation, h.GetObjectRetentionHandler))). Add(NewFilter(). Queries(s3middleware.LegalHoldQuery). Handler(named(s3middleware.GetObjectLegalHoldOperation, h.GetObjectLegalHoldHandler))). Add(NewFilter(). Queries(s3middleware.AttributesQuery). Handler(named(s3middleware.GetObjectAttributesOperation, h.GetObjectAttributesHandler))). DefaultHandler(named(s3middleware.GetObjectOperation, h.GetObjectHandler))) }) // PUT method handlers objRouter.Group(func(r chi.Router) { r.Method(http.MethodPut, "/*", NewHandlerFilter(). Add(NewFilter(). Headers(AmzCopySource). Queries(s3middleware.PartNumberQuery, s3middleware.UploadIDQuery). Handler(named(s3middleware.UploadPartCopyOperation, h.UploadPartCopy))). Add(NewFilter(). Queries(s3middleware.PartNumberQuery, s3middleware.UploadIDQuery). Handler(named(s3middleware.UploadPartOperation, h.UploadPartHandler))). Add(NewFilter(). Queries(s3middleware.ACLQuery). Handler(named(s3middleware.PutObjectACLOperation, h.PutObjectACLHandler))). Add(NewFilter(). Queries(s3middleware.TaggingQuery). Handler(named(s3middleware.PutObjectTaggingOperation, h.PutObjectTaggingHandler))). Add(NewFilter(). Headers(AmzCopySource). Handler(named(s3middleware.CopyObjectOperation, h.CopyObjectHandler))). Add(NewFilter(). Queries(s3middleware.RetentionQuery). Handler(named(s3middleware.PutObjectRetentionOperation, h.PutObjectRetentionHandler))). Add(NewFilter(). Queries(s3middleware.LegalHoldQuery). Handler(named(s3middleware.PutObjectLegalHoldOperation, h.PutObjectLegalHoldHandler))). DefaultHandler(named(s3middleware.PutObjectOperation, h.PutObjectHandler))) }) // POST method handlers objRouter.Group(func(r chi.Router) { r.Method(http.MethodPost, "/*", NewHandlerFilter(). Add(NewFilter(). Queries(s3middleware.UploadIDQuery). Handler(named(s3middleware.CompleteMultipartUploadOperation, h.CompleteMultipartUploadHandler))). Add(NewFilter(). Queries(s3middleware.UploadsQuery). Handler(named(s3middleware.CreateMultipartUploadOperation, h.CreateMultipartUploadHandler))). DefaultHandler(named(s3middleware.SelectObjectContentOperation, h.SelectObjectContentHandler))) }) // DELETE method handlers objRouter.Group(func(r chi.Router) { r.Method(http.MethodDelete, "/*", NewHandlerFilter(). Add(NewFilter(). Queries(s3middleware.UploadIDQuery). Handler(named(s3middleware.AbortMultipartUploadOperation, h.AbortMultipartUploadHandler))). Add(NewFilter(). Queries(s3middleware.TaggingQuery). Handler(named(s3middleware.DeleteObjectTaggingOperation, h.DeleteObjectTaggingHandler))). DefaultHandler(named(s3middleware.DeleteObjectOperation, h.DeleteObjectHandler))) }) attachErrorHandler(objRouter) return objRouter }