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" "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) } ) type Config struct { Throttle middleware.ThrottleOpts Handler Handler Center s3middleware.Center Log *zap.Logger Metrics *metrics.AppMetrics // Domains optional. If empty no virtual hosted domains will be attached. Domains []string // FrostfsID optional. If nil middleware.FrostfsIDValidation won't be attached. FrostfsID s3middleware.FrostFSID } func NewRouter(cfg Config) *chi.Mux { api := chi.NewRouter() api.Use( s3middleware.Request(cfg.Log), middleware.ThrottleWithOpts(cfg.Throttle), middleware.Recoverer, s3middleware.Tracing(), s3middleware.Metrics(cfg.Log, cfg.Handler.ResolveBucket, cfg.Metrics), s3middleware.LogSuccessResponse(cfg.Log), s3middleware.Auth(cfg.Center, cfg.Log), ) if cfg.FrostfsID != nil { api.Use(s3middleware.FrostfsIDValidation(cfg.FrostfsID, cfg.Log)) } defaultRouter := chi.NewRouter() defaultRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.BucketURLPrm), bucketRouter(cfg.Handler, cfg.Log)) defaultRouter.Get("/", named("ListBuckets", cfg.Handler.ListBucketsHandler)) hr := NewHostBucketRouter("bucket") hr.Default(defaultRouter) for _, domain := range cfg.Domains { hr.Map(domain, bucketRouter(cfg.Handler, cfg.Log)) } api.Mount("/", hr) attachErrorHandler(api) return api } 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) s3middleware.WriteErrorResponse(w, reqInfo, errors.Error{ Code: "UnknownAPIRequest", Description: desc, HTTPStatusCode: http.StatusBadRequest, }) if log := s3middleware.GetReqLog(ctx); log != nil { log.Error(logs.RequestUnmatched, zap.String("method", reqInfo.API)) } } // 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, log *zap.Logger) chi.Router { bktRouter := chi.NewRouter() bktRouter.Use( s3middleware.AddBucketName(log), s3middleware.WrapHandler(h.AppendCORSHeaders), ) bktRouter.Mount("/", 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("uploads"). 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))) }) attachErrorHandler(bktRouter) 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))) }) attachErrorHandler(objRouter) return objRouter }