forked from TrueCloudLab/frostfs-s3-gw
715 lines
22 KiB
Go
715 lines
22 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"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/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))
|
|
})
|
|
}
|
|
}
|
|
|
|
type HostBucketRouter struct {
|
|
routes map[string]chi.Router
|
|
bktParam string
|
|
defaultRouter chi.Router
|
|
}
|
|
|
|
func NewHostBucketRouter(bktParam string) HostBucketRouter {
|
|
return HostBucketRouter{
|
|
routes: make(map[string]chi.Router),
|
|
bktParam: bktParam,
|
|
}
|
|
}
|
|
|
|
func (hr *HostBucketRouter) Default(router chi.Router) {
|
|
hr.defaultRouter = router
|
|
}
|
|
|
|
func (hr HostBucketRouter) Map(host string, h chi.Router) {
|
|
hr.routes[strings.ToLower(host)] = h
|
|
}
|
|
|
|
func (hr HostBucketRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
bucket, domain := getBucketDomain(getHost(r))
|
|
router, ok := hr.routes[strings.ToLower(domain)]
|
|
if !ok {
|
|
router = hr.defaultRouter
|
|
if router == nil {
|
|
http.Error(w, http.StatusText(404), 404)
|
|
return
|
|
}
|
|
}
|
|
|
|
if rctx := chi.RouteContext(r.Context()); rctx != nil && bucket != "" {
|
|
rctx.URLParams.Add(hr.bktParam, bucket)
|
|
}
|
|
|
|
router.ServeHTTP(w, r)
|
|
}
|
|
|
|
func getBucketDomain(host string) (bucket string, domain string) {
|
|
parts := strings.Split(host, ".")
|
|
if len(parts) > 1 {
|
|
return parts[0], strings.Join(parts[1:], ".")
|
|
}
|
|
return "", host
|
|
}
|
|
|
|
// getHost tries its best to return the request host.
|
|
// According to section 14.23 of RFC 2616 the Host header
|
|
// can include the port number if the default value of 80 is not used.
|
|
func getHost(r *http.Request) string {
|
|
host := r.Host
|
|
if r.URL.IsAbs() {
|
|
host = r.URL.Host
|
|
}
|
|
|
|
if i := strings.Index(host, ":"); i != -1 {
|
|
host = host[:i]
|
|
}
|
|
|
|
return host
|
|
}
|
|
|
|
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),
|
|
)
|
|
|
|
defaultRouter := chi.NewRouter()
|
|
defaultRouter.Mount("/{bucket}", 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)
|
|
|
|
// 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)
|
|
}
|