frostfs-s3-gw/api/router.go
Denis Kirillov 5acbe60b78 [#XX] Use go-chi mux
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-01-14 15:52:17 +03:00

651 lines
21 KiB
Go

package api
import (
"context"
"fmt"
"net/http"
"net/url"
"sync"
"github.com/TrueCloudLab/frostfs-s3-gw/api/auth"
"github.com/TrueCloudLab/frostfs-s3-gw/api/errors"
"github.com/TrueCloudLab/frostfs-s3-gw/api/metrics"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/hostrouter"
"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)
}
// 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
// bucket name and object will be set in reqInfo later (limitation of go-chi)
r = r.WithContext(SetReqInfo(r.Context(), NewReqInfo(w, r, ObjectRequest{})))
// continue execution
h.ServeHTTP(w, r)
})
}
// addBucketName adds bucket name to ReqInfo from context.
func addBucketName(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqInfo := GetReqInfo(r.Context())
reqInfo.BucketName = chi.URLParam(r, "bucket")
h.ServeHTTP(w, r)
})
}
// addObjectName adds objects name to ReqInfo from context.
func addObjectName(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
obj := chi.URLParam(r, "object")
object, err := url.PathUnescape(obj)
if err != nil {
object = obj
}
prefix, err := url.QueryUnescape(chi.URLParam(r, "prefix"))
if err != nil {
prefix = chi.URLParam(r, "prefix")
}
if prefix != "" {
object = prefix
}
reqInfo := GetReqInfo(r.Context())
reqInfo.ObjectName = object
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 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())
// here reqInfo doesn't contain bucket name and object name
// pass execution:
h.ServeHTTP(lw, r)
// here reqInfo contains bucket name and object name because of
// addBucketName and addObjectName middlewares
// 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", reqInfo.API),
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")
}
}
func authMiddleware(center auth.Center, log *zap.Logger) func(h http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx context.Context
box, err := center.Authenticate(r)
if err != nil {
if err == auth.ErrNoAuthorizationHeader {
log.Debug("couldn't receive access box for gate key, random key will be used")
ctx = r.Context()
} else {
log.Error("failed to pass authentication", zap.Error(err))
if _, ok := err.(errors.Error); !ok {
err = errors.GetAPIError(errors.ErrAccessDenied)
}
WriteErrorResponse(w, GetReqInfo(r.Context()), err)
return
}
} else {
ctx = context.WithValue(r.Context(), BoxData, box.AccessBox)
if !box.ClientTime.IsZero() {
ctx = context.WithValue(ctx, ClientTime, box.ClientTime)
}
}
h.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func AttachChi(api *chi.Mux, domains []string, throttle middleware.ThrottleOpts, h Handler, center auth.Center, log *zap.Logger) {
api.Use(
middleware.CleanPath,
setRequestID,
logErrorResponse(log),
middleware.ThrottleWithOpts(throttle),
middleware.Recoverer,
authMiddleware(center, log),
)
// todo reconsider host routing
hr := hostrouter.New()
for _, domain := range domains {
hr.Map("*."+domain, bucketRouter(h, log))
}
api.Mount("/", hr)
api.Mount("/{bucket}", bucketRouter(h, log))
api.Get("/", h.ListBucketsHandler)
// If none of the routes match, add default error handler routes
api.NotFound(metrics.APIStats("notfound", errorResponseHandler))
api.MethodNotAllowed(metrics.APIStats("methodnotallowed", errorResponseHandler))
}
func Named(name string, handlerFunc http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
reqInfo := GetReqInfo(r.Context())
reqInfo.API = name
handlerFunc.ServeHTTP(w, r)
}
}
func bucketRouter(h Handler, log *zap.Logger) chi.Router {
bktRouter := chi.NewRouter()
bktRouter.Use(
addBucketName,
appendCORS(h),
)
bktRouter.Mount("/{object}", 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, log *zap.Logger) chi.Router {
objRouter := chi.NewRouter()
objRouter.Use(addObjectName)
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(hdrAmzCopySource).
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(hdrAmzCopySource).
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
}
type HandlerFilters struct {
filters []Filter
defaultHandler http.Handler
}
type Filter struct {
queries []Pair
headers []Pair
h http.Handler
}
type Pair struct {
Key string
Value string
}
func NewHandlerFilter() *HandlerFilters {
return &HandlerFilters{}
}
func NewFilter() *Filter {
return &Filter{}
}
func (hf *HandlerFilters) Add(filter *Filter) *HandlerFilters {
hf.filters = append(hf.filters, *filter)
return hf
}
// HeadersMatch adds a matcher for header values.
// It accepts a sequence of key/value pairs. Values may define variables.
// Panics if number of parameters is not even.
// Supports only exact matching.
// If the value is an empty string, it will match any value if the key is set.
func (f *Filter) HeadersMatch(pairs ...string) *Filter {
length := len(pairs)
if length%2 != 0 {
panic(fmt.Errorf("filter headers: number of parameters must be multiple of 2, got %v", pairs))
}
for i := 0; i < length; i += 2 {
f.headers = append(f.headers, Pair{
Key: pairs[i],
Value: pairs[i+1],
})
}
return f
}
// Headers is similar to HeadersMatch but accept only header keys, set value to empty string internally.
func (f *Filter) Headers(headers ...string) *Filter {
for _, header := range headers {
f.headers = append(f.headers, Pair{
Key: header,
Value: "",
})
}
return f
}
func (f *Filter) Handler(handler http.HandlerFunc) *Filter {
f.h = handler
return f
}
// QueriesMatch adds a matcher for URL query values.
// It accepts a sequence of key/value pairs. Values may define variables.
// Panics if number of parameters is not even.
// Supports only exact matching.
// If the value is an empty string, it will match any value if the key is set.
func (f *Filter) QueriesMatch(pairs ...string) *Filter {
length := len(pairs)
if length%2 != 0 {
panic(fmt.Errorf("filter headers: number of parameters must be multiple of 2, got %v", pairs))
}
for i := 0; i < length; i += 2 {
f.queries = append(f.queries, Pair{
Key: pairs[i],
Value: pairs[i+1],
})
}
return f
}
// Queries is similar to QueriesMatch but accept only query keys, set value to empty string internally.
func (f *Filter) Queries(queries ...string) *Filter {
for _, query := range queries {
f.queries = append(f.queries, Pair{
Key: query,
Value: "",
})
}
return f
}
func (hf *HandlerFilters) DefaultHandler(handler http.HandlerFunc) *HandlerFilters {
hf.defaultHandler = handler
return hf
}
func (hf *HandlerFilters) ServeHTTP(w http.ResponseWriter, r *http.Request) {
LOOP:
for _, filter := range hf.filters {
for _, header := range filter.headers {
hdrVals := r.Header.Values(header.Key)
if len(hdrVals) == 0 || header.Value != "" && header.Value != hdrVals[0] {
continue LOOP
}
}
for _, query := range filter.queries {
queryVal := r.URL.Query().Get(query.Key)
if !r.URL.Query().Has(query.Key) || queryVal != "" && query.Value != queryVal {
continue LOOP
}
}
filter.h.ServeHTTP(w, r)
return
}
hf.defaultHandler.ServeHTTP(w, r)
}