[TrueCloudLab#5] Refactor middlewares

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2022-12-27 15:30:11 +03:00 committed by Alex Vanin
parent c5570e661d
commit e278ab9362
4 changed files with 257 additions and 203 deletions

View file

@ -1,6 +1,7 @@
package api package api
import ( import (
"context"
"io" "io"
"net/http" "net/http"
"strings" "strings"
@ -35,6 +36,7 @@ func (t TrafficType) String() string {
type RequestType int type RequestType int
const ( const (
UNKNOWNRequest RequestType = iota
HEADRequest RequestType = iota HEADRequest RequestType = iota
PUTRequest RequestType = iota PUTRequest RequestType = iota
LISTRequest RequestType = iota LISTRequest RequestType = iota
@ -44,15 +46,15 @@ const (
func (t RequestType) String() string { func (t RequestType) String() string {
switch t { switch t {
case 0:
return "HEAD"
case 1: case 1:
return "PUT" return "HEAD"
case 2: case 2:
return "LIST" return "PUT"
case 3: case 3:
return "GET" return "LIST"
case 4: case 4:
return "GET"
case 5:
return "DELETE" return "DELETE"
default: default:
return "Unknown" return "Unknown"
@ -61,35 +63,34 @@ func (t RequestType) String() string {
func RequestTypeFromAPI(api string) RequestType { func RequestTypeFromAPI(api string) RequestType {
switch api { switch api {
case "headobject", "headbucket": case "Options", "HeadObject", "HeadBucket":
return HEADRequest return HEADRequest
case "createmultipartupload", "uploadpartcopy", "uploadpart", "completemutipartupload", case "CreateMultipartUpload", "UploadPartCopy", "UploadPart", "CompleteMultipartUpload",
"putobjectacl", "putobjecttagging", "copyobject", "putobjectretention", "putobjectlegalhold", "PutObjectACL", "PutObjectTagging", "CopyObject", "PutObjectRetention", "PutObjectLegalHold",
"putobject", "putbucketcors", "putbucketacl", "putbucketlifecycle", "putbucketencryption", "PutObject", "PutBucketCors", "PutBucketACL", "PutBucketLifecycle", "PutBucketEncryption",
"putbucketpolicy", "putbucketobjectlockconfig", "putbuckettagging", "putbucketversioning", "PutBucketPolicy", "PutBucketObjectLockConfig", "PutBucketTagging", "PutBucketVersioning",
"putbucketnotification", "createbucket", "postobject": "PutBucketNotification", "CreateBucket", "PostObject":
return PUTRequest return PUTRequest
case "listmultipartuploads", "listobjectsv2M", "listobjectsv2", "listbucketversions", case "ListObjectParts", "ListMultipartUploads", "ListObjectsV2M", "ListObjectsV2", "ListBucketVersions",
"listobjectsv1", "listbuckets": "ListObjectsV1", "ListBuckets":
return LISTRequest return LISTRequest
case "getobjectacl", "getobjecttagging", "getobjectretention", "getobjectlegalhold", case "GetObjectACL", "GetObjectTagging", "SelectObjectContent", "GetObjectRetention", "getobjectlegalhold",
"getobjectattributes", "getobject", "getbucketlocation", "getbucketpolicy", "GetObjectAttributes", "GetObject", "GetBucketLocation", "GetBucketPolicy",
"getbucketlifecycle", "getbucketencryption", "getbucketcors", "getbucketacl", "GetBucketLifecycle", "GetBucketEncryption", "GetBucketCors", "GetBucketACL",
"getbucketwebsite", "getbucketaccelerate", "getbucketrequestpayment", "getbucketlogging", "GetBucketWebsite", "GetBucketAccelerate", "GetBucketRequestPayment", "GetBucketLogging",
"getbucketreplication", "getbuckettagging", "selectobjectcontent", "GetBucketReplication", "GetBucketTagging", "GetBucketObjectLockConfig",
"getbucketobjectlockconfiguration", "getbucketversioning", "getbucketnotification", "GetBucketVersioning", "GetBucketNotification", "ListenBucketNotification":
"listenbucketnotification":
return GETRequest return GETRequest
case "abortmultipartupload", "deleteobjecttagging", "deleteobject", "deletebucketcors", case "AbortMultipartUpload", "DeleteObjectTagging", "DeleteObject", "DeleteBucketCors",
"deletebucketwebsite", "deletebuckettagging", "deletemultipleobjects", "deletebucketpolicy", "DeleteBucketWebsite", "DeleteBucketTagging", "DeleteMultipleObjects", "DeleteBucketPolicy",
"deletebucketlifecycle", "deletebucketencryption", "deletebucket": "DeleteBucketLifecycle", "DeleteBucketEncryption", "DeleteBucket":
return DELETERequest return DELETERequest
default: default:
return RequestType(-1) return UNKNOWNRequest
} }
} }
type OperationList [5]int type OperationList [6]int
type ( type (
// HTTPAPIStats holds statistics information about // HTTPAPIStats holds statistics information about
@ -262,11 +263,16 @@ func collectUserMetrics(ch chan<- prometheus.Metric) {
} }
} }
// APIStats wraps http handler for api with basic statistics collection. // CIDResolveFunc is a func to resolve CID in Stats handler.
func APIStats(api string, f http.HandlerFunc) http.HandlerFunc { type CIDResolveFunc func(ctx context.Context, reqInfo *ReqInfo) (cnrID string)
// Stats is a handler that update metrics.
func Stats(f http.HandlerFunc, resolveCID CIDResolveFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
httpStatsMetric.currentS3Requests.Inc(api) reqInfo := GetReqInfo(r.Context())
defer httpStatsMetric.currentS3Requests.Dec(api)
httpStatsMetric.currentS3Requests.Inc(reqInfo.API)
defer httpStatsMetric.currentS3Requests.Dec(reqInfo.API)
in := &readCounter{ReadCloser: r.Body} in := &readCounter{ReadCloser: r.Body}
out := &writeCounter{ResponseWriter: w} out := &writeCounter{ResponseWriter: w}
@ -278,20 +284,45 @@ func APIStats(api string, f http.HandlerFunc) http.HandlerFunc {
startTime: time.Now(), startTime: time.Now(),
} }
f.ServeHTTP(statsWriter, r) f(statsWriter, r)
// Time duration in secs since the call started. // Time duration in secs since the call started.
// We don't need to do nanosecond precision here // We don't need to do nanosecond precision here
// simply for the fact that it is not human readable. // simply for the fact that it is not human-readable.
durationSecs := time.Since(statsWriter.startTime).Seconds() durationSecs := time.Since(statsWriter.startTime).Seconds()
httpStatsMetric.updateStats(api, statsWriter, r, durationSecs, in.countBytes, out.countBytes) user := resolveUser(r.Context())
cnrID := resolveCID(r.Context(), reqInfo)
httpStatsMetric.usersS3Requests.Update(user, reqInfo.BucketName, cnrID, RequestTypeFromAPI(reqInfo.API), in.countBytes, out.countBytes)
code := statsWriter.statusCode
// A successful request has a 2xx response code
successReq := code >= http.StatusOK && code < http.StatusMultipleChoices
if !strings.HasSuffix(r.URL.Path, systemPath) {
httpStatsMetric.totalS3Requests.Inc(reqInfo.API)
if !successReq && code != 0 {
httpStatsMetric.totalS3Errors.Inc(reqInfo.API)
}
}
if r.Method == http.MethodGet {
// Increment the prometheus http request response histogram with appropriate label
httpRequestsDuration.With(prometheus.Labels{"api": reqInfo.API}).Observe(durationSecs)
}
atomic.AddUint64(&httpStatsMetric.totalInputBytes, in.countBytes) atomic.AddUint64(&httpStatsMetric.totalInputBytes, in.countBytes)
atomic.AddUint64(&httpStatsMetric.totalOutputBytes, out.countBytes) atomic.AddUint64(&httpStatsMetric.totalOutputBytes, out.countBytes)
} }
} }
func resolveUser(ctx context.Context) string {
user := "anon"
if bd, ok := ctx.Value(BoxData).(*accessbox.Box); ok && bd != nil && bd.Gate != nil && bd.Gate.BearerToken != nil {
user = bearer.ResolveIssuer(*bd.Gate.BearerToken).String()
}
return user
}
// Inc increments the api stats counter. // Inc increments the api stats counter.
func (stats *HTTPAPIStats) Inc(api string) { func (stats *HTTPAPIStats) Inc(api string) {
if stats == nil { if stats == nil {
@ -350,7 +381,7 @@ func (u *UsersAPIStats) Update(user, bucket, cnrID string, reqType RequestType,
} }
bktStat := usersStat.buckets[key] bktStat := usersStat.buckets[key]
bktStat.Operations[reqType] += 1 bktStat.Operations[reqType]++
bktStat.InTraffic += in bktStat.InTraffic += in
bktStat.OutTraffic += out bktStat.OutTraffic += out
usersStat.buckets[key] = bktStat usersStat.buckets[key] = bktStat
@ -414,40 +445,6 @@ func (st *HTTPStats) getOutputBytes() uint64 {
return atomic.LoadUint64(&st.totalOutputBytes) return atomic.LoadUint64(&st.totalOutputBytes)
} }
// Update statistics from http request and response data.
func (st *HTTPStats) updateStats(apiOperation string, w http.ResponseWriter, r *http.Request, durationSecs float64, in, out uint64) {
var code int
if res, ok := w.(*responseWrapper); ok {
code = res.statusCode
}
user := "anon"
if bd, ok := r.Context().Value(BoxData).(*accessbox.Box); ok && bd != nil && bd.Gate != nil && bd.Gate.BearerToken != nil {
user = bearer.ResolveIssuer(*bd.Gate.BearerToken).String()
}
reqInfo := GetReqInfo(r.Context())
cnrID := GetCID(r.Context())
st.usersS3Requests.Update(user, reqInfo.BucketName, cnrID, RequestTypeFromAPI(apiOperation), in, out)
// A successful request has a 2xx response code
successReq := code >= http.StatusOK && code < http.StatusMultipleChoices
if !strings.HasSuffix(r.URL.Path, systemPath) {
st.totalS3Requests.Inc(apiOperation)
if !successReq && code != 0 {
st.totalS3Errors.Inc(apiOperation)
}
}
if r.Method == http.MethodGet {
// Increment the prometheus http request response histogram with appropriate label
httpRequestsDuration.With(prometheus.Labels{"api": apiOperation}).Observe(durationSecs)
}
}
// WriteHeader -- writes http status code. // WriteHeader -- writes http status code.
func (w *responseWrapper) WriteHeader(code int) { func (w *responseWrapper) WriteHeader(code int) {
w.Do(func() { w.Do(func() {

View file

@ -2,7 +2,6 @@ package api
import ( import (
"context" "context"
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -43,13 +42,10 @@ type (
} }
) )
// Key used for Get/Set context values. // Key used for Get/SetReqInfo.
type contextKeyType string type contextKeyType string
const ( const ctxRequestInfo = contextKeyType("FrostFS-S3-GW")
ctxRequestInfo = contextKeyType("FrostFS-S3-GW")
ctxCID = contextKeyType("FrostFS-S3-GW-CID")
)
var ( var (
// De-facto standard header keys. // De-facto standard header keys.
@ -206,21 +202,3 @@ func GetReqInfo(ctx context.Context) *ReqInfo {
} }
return &ReqInfo{} return &ReqInfo{}
} }
// SetCID sets CID in the context.
func SetCID(ctx context.Context, id cid.ID) context.Context {
if ctx == nil {
return nil
}
return context.WithValue(ctx, ctxCID, id.EncodeToString())
}
// GetCID returns CID if set.
func GetCID(ctx context.Context) string {
if ctx == nil {
return ""
} else if id, ok := ctx.Value(ctxCID).(string); ok {
return id
}
return ""
}

View file

@ -108,7 +108,7 @@ const (
MimeXML mimeType = "application/xml" MimeXML mimeType = "application/xml"
) )
var _ = logErrorResponse var _ = logSuccessResponse
func (lrw *logResponseWriter) WriteHeader(code int) { func (lrw *logResponseWriter) WriteHeader(code int) {
lrw.Do(func() { lrw.Do(func() {
@ -147,33 +147,37 @@ func appendCORS(handler Handler) mux.MiddlewareFunc {
} }
} }
func resolveBucket(log *zap.Logger, resolveBucket func(ctx context.Context, bucket string) (*data.BucketInfo, error)) mux.MiddlewareFunc { // BucketResolveFunc is a func to resolve bucket info by name.
return func(h http.Handler) http.Handler { type BucketResolveFunc func(ctx context.Context, bucket string) (*data.BucketInfo, error)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqInfo := GetReqInfo(r.Context())
if reqInfo.BucketName != "" && reqInfo.API != "CreateBucket" { // metricsMiddleware wraps http handler for api with basic statistics collection.
bktInfo, err := resolveBucket(r.Context(), reqInfo.BucketName) func metricsMiddleware(log *zap.Logger, resolveBucket BucketResolveFunc) mux.MiddlewareFunc {
return func(h http.Handler) http.Handler {
return Stats(h.ServeHTTP, resolveCID(log, resolveBucket))
}
}
// resolveCID forms CIDResolveFunc using BucketResolveFunc.
func resolveCID(log *zap.Logger, resolveBucket BucketResolveFunc) CIDResolveFunc {
return func(ctx context.Context, reqInfo *ReqInfo) (cnrID string) {
if reqInfo.BucketName == "" || reqInfo.API == "CreateBucket" || reqInfo.API == "" {
return ""
}
bktInfo, err := resolveBucket(ctx, reqInfo.BucketName)
if err != nil { if err != nil {
code := WriteErrorResponse(w, reqInfo, err) log.Debug("failed to resolve CID",
log.Error("failed to resolve bucket", zap.Int("status", code),
zap.String("request_id", reqInfo.RequestID), zap.String("method", reqInfo.API), zap.String("request_id", reqInfo.RequestID), zap.String("method", reqInfo.API),
zap.String("bucket", reqInfo.BucketName), zap.String("object", reqInfo.ObjectName), zap.String("bucket", reqInfo.BucketName), zap.String("object", reqInfo.ObjectName),
zap.Error(err)) zap.Error(err))
return return ""
} }
// todo: (@KirillovDenis) consider save bktInfo into ReqInfo return bktInfo.CID.EncodeToString()
// (in order to optimize resolving bucket in further handlers)
r = r.WithContext(SetCID(r.Context(), bktInfo.CID))
}
h.ServeHTTP(w, r)
})
} }
} }
func logErrorResponse(l *zap.Logger) mux.MiddlewareFunc { func logSuccessResponse(l *zap.Logger) mux.MiddlewareFunc {
return func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lw := &logResponseWriter{ResponseWriter: w} lw := &logResponseWriter{ResponseWriter: w}
@ -211,6 +215,30 @@ func GetRequestID(v interface{}) string {
} }
} }
func setErrorAPI(apiName string, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := SetReqInfo(r.Context(), &ReqInfo{API: apiName})
h.ServeHTTP(w, r.WithContext(ctx))
})
}
// attachErrorHandler set NotFoundHandler and MethodNotAllowedHandler for mux.Router.
func attachErrorHandler(api *mux.Router, log *zap.Logger, h Handler, center auth.Center) {
middlewares := []mux.MiddlewareFunc{
AuthMiddleware(log, center),
metricsMiddleware(log, h.ResolveBucket),
}
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.NotFoundHandler = setErrorAPI("NotFound", errorHandler)
api.MethodNotAllowedHandler = setErrorAPI("MethodNotAllowed", errorHandler)
}
// Attach adds S3 API handlers from h to r for domains with m client limit using // Attach adds S3 API handlers from h to r for domains with m client limit using
// center authentication and log logger. // center authentication and log logger.
func Attach(r *mux.Router, domains []string, m MaxClients, h Handler, center auth.Center, log *zap.Logger) { func Attach(r *mux.Router, domains []string, m MaxClients, h Handler, center auth.Center, log *zap.Logger) {
@ -220,15 +248,16 @@ func Attach(r *mux.Router, domains []string, m MaxClients, h Handler, center aut
// -- prepare request // -- prepare request
setRequestID, setRequestID,
// -- resolve bucket to set cid in context // Attach user authentication for all S3 routes.
resolveBucket(log, h.ResolveBucket), AuthMiddleware(log, center),
metricsMiddleware(log, h.ResolveBucket),
// -- logging error requests // -- logging error requests
logErrorResponse(log), logSuccessResponse(log),
) )
// Attach user authentication for all S3 routes. attachErrorHandler(api, log, h, center)
AttachUserAuth(api, center, log)
buckets := make([]*mux.Router, 0, len(domains)+1) buckets := make([]*mux.Router, 0, len(domains)+1)
buckets = append(buckets, api.PathPrefix("/{bucket}").Subrouter()) buckets = append(buckets, api.PathPrefix("/{bucket}").Subrouter())
@ -244,277 +273,327 @@ func Attach(r *mux.Router, domains []string, m MaxClients, h Handler, center aut
// -- append CORS headers to a response for // -- append CORS headers to a response for
appendCORS(h), appendCORS(h),
) )
bucket.Methods(http.MethodOptions).HandlerFunc(m.Handle(APIStats("preflight", h.Preflight))).Name("Options") bucket.Methods(http.MethodOptions).HandlerFunc(
m.Handle(h.Preflight)).
Name("Options")
bucket.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("headobject", h.HeadObjectHandler))).Name("HeadObject") m.Handle(h.HeadObjectHandler)).
Name("HeadObject")
// CopyObjectPart // CopyObjectPart
bucket.Methods(http.MethodPut).Path("/{object:.+}").Headers(hdrAmzCopySource, "").HandlerFunc(m.Handle(APIStats("uploadpartcopy", h.UploadPartCopy))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}"). bucket.Methods(http.MethodPut).Path("/{object:.+}").Headers(hdrAmzCopySource, "").HandlerFunc(
m.Handle(h.UploadPartCopy)).
Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}").
Name("UploadPartCopy") Name("UploadPartCopy")
// PutObjectPart // PutObjectPart
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("uploadpart", h.UploadPartHandler))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}"). m.Handle(h.UploadPartHandler)).
Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}").
Name("UploadPart") Name("UploadPart")
// ListParts // ListParts
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("listobjectparts", h.ListPartsHandler))).Queries("uploadId", "{uploadId:.*}"). m.Handle(h.ListPartsHandler)).
Queries("uploadId", "{uploadId:.*}").
Name("ListObjectParts") Name("ListObjectParts")
// CompleteMultipartUpload // CompleteMultipartUpload
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("completemutipartupload", h.CompleteMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}"). m.Handle(h.CompleteMultipartUploadHandler)).
Queries("uploadId", "{uploadId:.*}").
Name("CompleteMultipartUpload") Name("CompleteMultipartUpload")
// CreateMultipartUpload // CreateMultipartUpload
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("createmultipartupload", h.CreateMultipartUploadHandler))).Queries("uploads", ""). m.Handle(h.CreateMultipartUploadHandler)).
Queries("uploads", "").
Name("CreateMultipartUpload") Name("CreateMultipartUpload")
// AbortMultipartUpload // AbortMultipartUpload
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("abortmultipartupload", h.AbortMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}"). m.Handle(h.AbortMultipartUploadHandler)).
Queries("uploadId", "{uploadId:.*}").
Name("AbortMultipartUpload") Name("AbortMultipartUpload")
// ListMultipartUploads // ListMultipartUploads
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("listmultipartuploads", h.ListMultipartUploadsHandler))).Queries("uploads", ""). m.Handle(h.ListMultipartUploadsHandler)).
Queries("uploads", "").
Name("ListMultipartUploads") Name("ListMultipartUploads")
// GetObjectACL -- this is a dummy call. // GetObjectACL -- this is a dummy call.
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("getobjectacl", h.GetObjectACLHandler))).Queries("acl", ""). m.Handle(h.GetObjectACLHandler)).
Queries("acl", "").
Name("GetObjectACL") Name("GetObjectACL")
// PutObjectACL -- this is a dummy call. // PutObjectACL -- this is a dummy call.
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("putobjectacl", h.PutObjectACLHandler))).Queries("acl", ""). m.Handle(h.PutObjectACLHandler)).
Queries("acl", "").
Name("PutObjectACL") Name("PutObjectACL")
// GetObjectTagging // GetObjectTagging
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("getobjecttagging", h.GetObjectTaggingHandler))).Queries("tagging", ""). m.Handle(h.GetObjectTaggingHandler)).
Queries("tagging", "").
Name("GetObjectTagging") Name("GetObjectTagging")
// PutObjectTagging // PutObjectTagging
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("putobjecttagging", h.PutObjectTaggingHandler))).Queries("tagging", ""). m.Handle(h.PutObjectTaggingHandler)).
Queries("tagging", "").
Name("PutObjectTagging") Name("PutObjectTagging")
// DeleteObjectTagging // DeleteObjectTagging
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("deleteobjecttagging", h.DeleteObjectTaggingHandler))).Queries("tagging", ""). m.Handle(h.DeleteObjectTaggingHandler)).
Queries("tagging", "").
Name("DeleteObjectTagging") Name("DeleteObjectTagging")
// SelectObjectContent // SelectObjectContent
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("selectobjectcontent", h.SelectObjectContentHandler))).Queries("select", "").Queries("select-type", "2"). m.Handle(h.SelectObjectContentHandler)).
Queries("select", "").Queries("select-type", "2").
Name("SelectObjectContent") Name("SelectObjectContent")
// GetObjectRetention // GetObjectRetention
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("getobjectretention", h.GetObjectRetentionHandler))).Queries("retention", ""). m.Handle(h.GetObjectRetentionHandler)).
Queries("retention", "").
Name("GetObjectRetention") Name("GetObjectRetention")
// GetObjectLegalHold // GetObjectLegalHold
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("getobjectlegalhold", h.GetObjectLegalHoldHandler))).Queries("legal-hold", ""). m.Handle(h.GetObjectLegalHoldHandler)).
Queries("legal-hold", "").
Name("GetObjectLegalHold") Name("GetObjectLegalHold")
// GetObjectAttributes // GetObjectAttributes
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("getobjectattributes", h.GetObjectAttributesHandler))).Queries("attributes", ""). m.Handle(h.GetObjectAttributesHandler)).
Queries("attributes", "").
Name("GetObjectAttributes") Name("GetObjectAttributes")
// GetObject // GetObject
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("getobject", h.GetObjectHandler))). m.Handle(h.GetObjectHandler)).
Name("GetObject") Name("GetObject")
// CopyObject // CopyObject
bucket.Methods(http.MethodPut).Path("/{object:.+}").Headers(hdrAmzCopySource, "").HandlerFunc(m.Handle(APIStats("copyobject", h.CopyObjectHandler))). bucket.Methods(http.MethodPut).Path("/{object:.+}").Headers(hdrAmzCopySource, "").HandlerFunc(
m.Handle(h.CopyObjectHandler)).
Name("CopyObject") Name("CopyObject")
// PutObjectRetention // PutObjectRetention
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("putobjectretention", h.PutObjectRetentionHandler))).Queries("retention", ""). m.Handle(h.PutObjectRetentionHandler)).
Queries("retention", "").
Name("PutObjectRetention") Name("PutObjectRetention")
// PutObjectLegalHold // PutObjectLegalHold
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("putobjectlegalhold", h.PutObjectLegalHoldHandler))).Queries("legal-hold", ""). m.Handle(h.PutObjectLegalHoldHandler)).
Queries("legal-hold", "").
Name("PutObjectLegalHold") Name("PutObjectLegalHold")
// PutObject // PutObject
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("putobject", h.PutObjectHandler))). m.Handle(h.PutObjectHandler)).
Name("PutObject") Name("PutObject")
// DeleteObject // DeleteObject
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc( bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
m.Handle(APIStats("deleteobject", h.DeleteObjectHandler))). m.Handle(h.DeleteObjectHandler)).
Name("DeleteObject") Name("DeleteObject")
// Bucket operations // Bucket operations
// GetBucketLocation // GetBucketLocation
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketlocation", h.GetBucketLocationHandler))).Queries("location", ""). m.Handle(h.GetBucketLocationHandler)).
Queries("location", "").
Name("GetBucketLocation") Name("GetBucketLocation")
// GetBucketPolicy // GetBucketPolicy
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketpolicy", h.GetBucketPolicyHandler))).Queries("policy", ""). m.Handle(h.GetBucketPolicyHandler)).
Queries("policy", "").
Name("GetBucketPolicy") Name("GetBucketPolicy")
// GetBucketLifecycle // GetBucketLifecycle
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketlifecycle", h.GetBucketLifecycleHandler))).Queries("lifecycle", ""). m.Handle(h.GetBucketLifecycleHandler)).
Queries("lifecycle", "").
Name("GetBucketLifecycle") Name("GetBucketLifecycle")
// GetBucketEncryption // GetBucketEncryption
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketencryption", h.GetBucketEncryptionHandler))).Queries("encryption", ""). m.Handle(h.GetBucketEncryptionHandler)).
Queries("encryption", "").
Name("GetBucketEncryption") Name("GetBucketEncryption")
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketcors", h.GetBucketCorsHandler))).Queries("cors", ""). m.Handle(h.GetBucketCorsHandler)).
Queries("cors", "").
Name("GetBucketCors") Name("GetBucketCors")
bucket.Methods(http.MethodPut).HandlerFunc( bucket.Methods(http.MethodPut).HandlerFunc(
m.Handle(APIStats("putbucketcors", h.PutBucketCorsHandler))).Queries("cors", ""). m.Handle(h.PutBucketCorsHandler)).
Queries("cors", "").
Name("PutBucketCors") Name("PutBucketCors")
bucket.Methods(http.MethodDelete).HandlerFunc( bucket.Methods(http.MethodDelete).HandlerFunc(
m.Handle(APIStats("deletebucketcors", h.DeleteBucketCorsHandler))).Queries("cors", ""). m.Handle(h.DeleteBucketCorsHandler)).
Queries("cors", "").
Name("DeleteBucketCors") Name("DeleteBucketCors")
// Dummy Bucket Calls // Dummy Bucket Calls
// GetBucketACL -- this is a dummy call. // GetBucketACL -- this is a dummy call.
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketacl", h.GetBucketACLHandler))).Queries("acl", ""). m.Handle(h.GetBucketACLHandler)).
Queries("acl", "").
Name("GetBucketACL") Name("GetBucketACL")
// PutBucketACL -- this is a dummy call. // PutBucketACL -- this is a dummy call.
bucket.Methods(http.MethodPut).HandlerFunc( bucket.Methods(http.MethodPut).HandlerFunc(
m.Handle(APIStats("putbucketacl", h.PutBucketACLHandler))).Queries("acl", ""). m.Handle(h.PutBucketACLHandler)).
Queries("acl", "").
Name("PutBucketACL") Name("PutBucketACL")
// GetBucketWebsiteHandler -- this is a dummy call. // GetBucketWebsiteHandler -- this is a dummy call.
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketwebsite", h.GetBucketWebsiteHandler))).Queries("website", ""). m.Handle(h.GetBucketWebsiteHandler)).
Queries("website", "").
Name("GetBucketWebsite") Name("GetBucketWebsite")
// GetBucketAccelerateHandler -- this is a dummy call. // GetBucketAccelerateHandler -- this is a dummy call.
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketaccelerate", h.GetBucketAccelerateHandler))).Queries("accelerate", ""). m.Handle(h.GetBucketAccelerateHandler)).
Queries("accelerate", "").
Name("GetBucketAccelerate") Name("GetBucketAccelerate")
// GetBucketRequestPaymentHandler -- this is a dummy call. // GetBucketRequestPaymentHandler -- this is a dummy call.
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketrequestpayment", h.GetBucketRequestPaymentHandler))).Queries("requestPayment", ""). m.Handle(h.GetBucketRequestPaymentHandler)).
Queries("requestPayment", "").
Name("GetBucketRequestPayment") Name("GetBucketRequestPayment")
// GetBucketLoggingHandler -- this is a dummy call. // GetBucketLoggingHandler -- this is a dummy call.
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketlogging", h.GetBucketLoggingHandler))).Queries("logging", ""). m.Handle(h.GetBucketLoggingHandler)).
Queries("logging", "").
Name("GetBucketLogging") Name("GetBucketLogging")
// GetBucketLifecycleHandler -- this is a dummy call.
bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketlifecycle", h.GetBucketLifecycleHandler))).Queries("lifecycle", "").
Name("GetBucketLifecycle")
// GetBucketReplicationHandler -- this is a dummy call. // GetBucketReplicationHandler -- this is a dummy call.
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketreplication", h.GetBucketReplicationHandler))).Queries("replication", ""). m.Handle(h.GetBucketReplicationHandler)).
Queries("replication", "").
Name("GetBucketReplication") Name("GetBucketReplication")
// GetBucketTaggingHandler // GetBucketTaggingHandler
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbuckettagging", h.GetBucketTaggingHandler))).Queries("tagging", ""). m.Handle(h.GetBucketTaggingHandler)).
Queries("tagging", "").
Name("GetBucketTagging") Name("GetBucketTagging")
// DeleteBucketWebsiteHandler // DeleteBucketWebsiteHandler
bucket.Methods(http.MethodDelete).HandlerFunc( bucket.Methods(http.MethodDelete).HandlerFunc(
m.Handle(APIStats("deletebucketwebsite", h.DeleteBucketWebsiteHandler))).Queries("website", ""). m.Handle(h.DeleteBucketWebsiteHandler)).
Queries("website", "").
Name("DeleteBucketWebsite") Name("DeleteBucketWebsite")
// DeleteBucketTaggingHandler // DeleteBucketTaggingHandler
bucket.Methods(http.MethodDelete).HandlerFunc( bucket.Methods(http.MethodDelete).HandlerFunc(
m.Handle(APIStats("deletebuckettagging", h.DeleteBucketTaggingHandler))).Queries("tagging", ""). m.Handle(h.DeleteBucketTaggingHandler)).
Queries("tagging", "").
Name("DeleteBucketTagging") Name("DeleteBucketTagging")
// GetBucketObjectLockConfig // GetBucketObjectLockConfig
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketobjectlockconfiguration", h.GetBucketObjectLockConfigHandler))).Queries("object-lock", ""). m.Handle(h.GetBucketObjectLockConfigHandler)).
Queries("object-lock", "").
Name("GetBucketObjectLockConfig") Name("GetBucketObjectLockConfig")
// GetBucketVersioning // GetBucketVersioning
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketversioning", h.GetBucketVersioningHandler))).Queries("versioning", ""). m.Handle(h.GetBucketVersioningHandler)).
Queries("versioning", "").
Name("GetBucketVersioning") Name("GetBucketVersioning")
// GetBucketNotification // GetBucketNotification
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("getbucketnotification", h.GetBucketNotificationHandler))).Queries("notification", ""). m.Handle(h.GetBucketNotificationHandler)).
Queries("notification", "").
Name("GetBucketNotification") Name("GetBucketNotification")
// ListenBucketNotification // ListenBucketNotification
bucket.Methods(http.MethodGet).HandlerFunc(APIStats("listenbucketnotification", h.ListenBucketNotificationHandler)).Queries("events", "{events:.*}"). bucket.Methods(http.MethodGet).HandlerFunc(h.ListenBucketNotificationHandler).
Queries("events", "{events:.*}").
Name("ListenBucketNotification") Name("ListenBucketNotification")
// ListObjectsV2M // ListObjectsV2M
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("listobjectsv2M", h.ListObjectsV2MHandler))).Queries("list-type", "2", "metadata", "true"). m.Handle(h.ListObjectsV2MHandler)).
Queries("list-type", "2", "metadata", "true").
Name("ListObjectsV2M") Name("ListObjectsV2M")
// ListObjectsV2 // ListObjectsV2
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("listobjectsv2", h.ListObjectsV2Handler))).Queries("list-type", "2"). m.Handle(h.ListObjectsV2Handler)).
Queries("list-type", "2").
Name("ListObjectsV2") Name("ListObjectsV2")
// ListBucketVersions // ListBucketVersions
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("listbucketversions", h.ListBucketObjectVersionsHandler))).Queries("versions", ""). m.Handle(h.ListBucketObjectVersionsHandler)).
Queries("versions", "").
Name("ListBucketVersions") Name("ListBucketVersions")
// ListObjectsV1 (Legacy) // ListObjectsV1 (Legacy)
bucket.Methods(http.MethodGet).HandlerFunc( bucket.Methods(http.MethodGet).HandlerFunc(
m.Handle(APIStats("listobjectsv1", h.ListObjectsV1Handler))). m.Handle(h.ListObjectsV1Handler)).
Name("ListObjectsV1") Name("ListObjectsV1")
// PutBucketLifecycle // PutBucketLifecycle
bucket.Methods(http.MethodPut).HandlerFunc( bucket.Methods(http.MethodPut).HandlerFunc(
m.Handle(APIStats("putbucketlifecycle", h.PutBucketLifecycleHandler))).Queries("lifecycle", ""). m.Handle(h.PutBucketLifecycleHandler)).
Queries("lifecycle", "").
Name("PutBucketLifecycle") Name("PutBucketLifecycle")
// PutBucketEncryption // PutBucketEncryption
bucket.Methods(http.MethodPut).HandlerFunc( bucket.Methods(http.MethodPut).HandlerFunc(
m.Handle(APIStats("putbucketencryption", h.PutBucketEncryptionHandler))).Queries("encryption", ""). m.Handle(h.PutBucketEncryptionHandler)).
Queries("encryption", "").
Name("PutBucketEncryption") Name("PutBucketEncryption")
// PutBucketPolicy // PutBucketPolicy
bucket.Methods(http.MethodPut).HandlerFunc( bucket.Methods(http.MethodPut).HandlerFunc(
m.Handle(APIStats("putbucketpolicy", h.PutBucketPolicyHandler))).Queries("policy", ""). m.Handle(h.PutBucketPolicyHandler)).
Queries("policy", "").
Name("PutBucketPolicy") Name("PutBucketPolicy")
// PutBucketObjectLockConfig // PutBucketObjectLockConfig
bucket.Methods(http.MethodPut).HandlerFunc( bucket.Methods(http.MethodPut).HandlerFunc(
m.Handle(APIStats("putbucketobjectlockconfig", h.PutBucketObjectLockConfigHandler))).Queries("object-lock", ""). m.Handle(h.PutBucketObjectLockConfigHandler)).
Queries("object-lock", "").
Name("PutBucketObjectLockConfig") Name("PutBucketObjectLockConfig")
// PutBucketTaggingHandler // PutBucketTaggingHandler
bucket.Methods(http.MethodPut).HandlerFunc( bucket.Methods(http.MethodPut).HandlerFunc(
m.Handle(APIStats("putbuckettagging", h.PutBucketTaggingHandler))).Queries("tagging", ""). m.Handle(h.PutBucketTaggingHandler)).
Queries("tagging", "").
Name("PutBucketTagging") Name("PutBucketTagging")
// PutBucketVersioning // PutBucketVersioning
bucket.Methods(http.MethodPut).HandlerFunc( bucket.Methods(http.MethodPut).HandlerFunc(
m.Handle(APIStats("putbucketversioning", h.PutBucketVersioningHandler))).Queries("versioning", ""). m.Handle(h.PutBucketVersioningHandler)).
Queries("versioning", "").
Name("PutBucketVersioning") Name("PutBucketVersioning")
// PutBucketNotification // PutBucketNotification
bucket.Methods(http.MethodPut).HandlerFunc( bucket.Methods(http.MethodPut).HandlerFunc(
m.Handle(APIStats("putbucketnotification", h.PutBucketNotificationHandler))).Queries("notification", ""). m.Handle(h.PutBucketNotificationHandler)).
Queries("notification", "").
Name("PutBucketNotification") Name("PutBucketNotification")
// CreateBucket // CreateBucket
bucket.Methods(http.MethodPut).HandlerFunc( bucket.Methods(http.MethodPut).HandlerFunc(
m.Handle(APIStats("createbucket", h.CreateBucketHandler))). m.Handle(h.CreateBucketHandler)).
Name("CreateBucket") Name("CreateBucket")
// HeadBucket // HeadBucket
bucket.Methods(http.MethodHead).HandlerFunc( bucket.Methods(http.MethodHead).HandlerFunc(
m.Handle(APIStats("headbucket", h.HeadBucketHandler))). m.Handle(h.HeadBucketHandler)).
Name("HeadBucket") Name("HeadBucket")
// PostPolicy // PostPolicy
bucket.Methods(http.MethodPost).HeadersRegexp(hdrContentType, "multipart/form-data*").HandlerFunc( bucket.Methods(http.MethodPost).HeadersRegexp(hdrContentType, "multipart/form-data*").HandlerFunc(
m.Handle(APIStats("postobject", h.PostObject))). m.Handle(h.PostObject)).
Name("PostObject") Name("PostObject")
// DeleteMultipleObjects // DeleteMultipleObjects
bucket.Methods(http.MethodPost).HandlerFunc( bucket.Methods(http.MethodPost).HandlerFunc(
m.Handle(APIStats("deletemultipleobjects", h.DeleteMultipleObjectsHandler))).Queries("delete", ""). m.Handle(h.DeleteMultipleObjectsHandler)).
Queries("delete", "").
Name("DeleteMultipleObjects") Name("DeleteMultipleObjects")
// DeleteBucketPolicy // DeleteBucketPolicy
bucket.Methods(http.MethodDelete).HandlerFunc( bucket.Methods(http.MethodDelete).HandlerFunc(
m.Handle(APIStats("deletebucketpolicy", h.DeleteBucketPolicyHandler))).Queries("policy", ""). m.Handle(h.DeleteBucketPolicyHandler)).
Queries("policy", "").
Name("DeleteBucketPolicy") Name("DeleteBucketPolicy")
// DeleteBucketLifecycle // DeleteBucketLifecycle
bucket.Methods(http.MethodDelete).HandlerFunc( bucket.Methods(http.MethodDelete).HandlerFunc(
m.Handle(APIStats("deletebucketlifecycle", h.DeleteBucketLifecycleHandler))).Queries("lifecycle", ""). m.Handle(h.DeleteBucketLifecycleHandler)).
Queries("lifecycle", "").
Name("DeleteBucketLifecycle") Name("DeleteBucketLifecycle")
// DeleteBucketEncryption // DeleteBucketEncryption
bucket.Methods(http.MethodDelete).HandlerFunc( bucket.Methods(http.MethodDelete).HandlerFunc(
m.Handle(APIStats("deletebucketencryption", h.DeleteBucketEncryptionHandler))).Queries("encryption", ""). m.Handle(h.DeleteBucketEncryptionHandler)).
Queries("encryption", "").
Name("DeleteBucketEncryption") Name("DeleteBucketEncryption")
// DeleteBucket // DeleteBucket
bucket.Methods(http.MethodDelete).HandlerFunc( bucket.Methods(http.MethodDelete).HandlerFunc(
m.Handle(APIStats("deletebucket", h.DeleteBucketHandler))). m.Handle(h.DeleteBucketHandler)).
Name("DeleteBucket") Name("DeleteBucket")
} }
// Root operation // Root operation
// ListBuckets // ListBuckets
api.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc( api.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
m.Handle(APIStats("listbuckets", h.ListBucketsHandler))). m.Handle(h.ListBucketsHandler)).
Name("ListBuckets") Name("ListBuckets")
// S3 browser with signature v4 adds '//' for ListBuckets request, so rather // S3 browser with signature v4 adds '//' for ListBuckets request, so rather
// than failing with UnknownAPIRequest we simply handle it for now. // than failing with UnknownAPIRequest we simply handle it for now.
api.Methods(http.MethodGet).Path(SlashSeparator + SlashSeparator).HandlerFunc( api.Methods(http.MethodGet).Path(SlashSeparator + SlashSeparator).HandlerFunc(
m.Handle(APIStats("listbuckets", h.ListBucketsHandler))). m.Handle(h.ListBucketsHandler)).
Name("ListBuckets") Name("ListBuckets")
// If none of the routes match, add default error handler routes
api.NotFoundHandler = APIStats("notfound", errorResponseHandler)
api.MethodNotAllowedHandler = APIStats("methodnotallowed", errorResponseHandler)
} }

View file

@ -19,9 +19,9 @@ var BoxData = KeyWrapper("__context_box_key")
// ClientTime is an ID used to store client time.Time in a context. // ClientTime is an ID used to store client time.Time in a context.
var ClientTime = KeyWrapper("__context_client_time") var ClientTime = KeyWrapper("__context_client_time")
// AttachUserAuth adds user authentication via center to router using log for logging. // AuthMiddleware adds user authentication via center to router using log for logging.
func AttachUserAuth(router *mux.Router, center auth.Center, log *zap.Logger) { func AuthMiddleware(log *zap.Logger, center auth.Center) mux.MiddlewareFunc {
router.Use(func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx context.Context var ctx context.Context
box, err := center.Authenticate(r) box, err := center.Authenticate(r)
@ -46,5 +46,5 @@ func AttachUserAuth(router *mux.Router, center auth.Center, log *zap.Logger) {
h.ServeHTTP(w, r.WithContext(ctx)) h.ServeHTTP(w, r.WithContext(ctx))
}) })
}) }
} }