forked from TrueCloudLab/frostfs-s3-gw
319 lines
8.8 KiB
Go
319 lines
8.8 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
|
treepool "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool/tree"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
"google.golang.org/grpc/metadata"
|
|
)
|
|
|
|
type (
|
|
// KeyVal -- appended to ReqInfo.Tags.
|
|
KeyVal struct {
|
|
Key string
|
|
Val string
|
|
}
|
|
|
|
// ReqInfo stores the request info.
|
|
ReqInfo struct {
|
|
sync.RWMutex
|
|
RemoteHost string // Client Host/IP
|
|
Host string // Node Host/IP
|
|
UserAgent string // User Agent
|
|
DeploymentID string // random generated s3-deployment-id
|
|
RequestID string // x-amz-request-id
|
|
API string // API name -- GetObject PutObject NewMultipartUpload etc.
|
|
BucketName string // Bucket name
|
|
ObjectName string // Object name
|
|
TraceID string // Trace ID
|
|
URL *url.URL // Request url
|
|
Namespace string
|
|
User string // User owner id
|
|
tags []KeyVal // Any additional info not accommodated by above fields
|
|
}
|
|
|
|
// ObjectRequest represents object request data.
|
|
ObjectRequest struct {
|
|
Bucket string
|
|
Object string
|
|
Method string
|
|
}
|
|
|
|
// Key used for custom key/value in context.
|
|
contextKeyType string
|
|
)
|
|
|
|
const (
|
|
ctxRequestInfo = contextKeyType("FrostFS-S3-GW")
|
|
ctxRequestLogger = contextKeyType("FrostFS-S3-GW-Logger")
|
|
)
|
|
|
|
const HdrAmzRequestID = "x-amz-request-id"
|
|
|
|
const (
|
|
BucketURLPrm = "bucket"
|
|
)
|
|
|
|
var deploymentID = uuid.Must(uuid.NewRandom())
|
|
|
|
var (
|
|
// De-facto standard header keys.
|
|
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
|
|
xRealIP = http.CanonicalHeaderKey("X-Real-IP")
|
|
|
|
// RFC7239 defines a new "Forwarded: " header designed to replace the
|
|
// existing use of X-Forwarded-* headers.
|
|
// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43.
|
|
forwarded = http.CanonicalHeaderKey("Forwarded")
|
|
// Allows for a sub-match of the first value after 'for=' to the next
|
|
// comma, semi-colon or space. The match is case-insensitive.
|
|
forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|, )]+)(.*)`)
|
|
)
|
|
|
|
// NewReqInfo returns new ReqInfo based on parameters.
|
|
func NewReqInfo(w http.ResponseWriter, r *http.Request, req ObjectRequest) *ReqInfo {
|
|
return &ReqInfo{
|
|
API: req.Method,
|
|
BucketName: req.Bucket,
|
|
ObjectName: req.Object,
|
|
UserAgent: r.UserAgent(),
|
|
RemoteHost: getSourceIP(r),
|
|
RequestID: GetRequestID(w),
|
|
DeploymentID: deploymentID.String(),
|
|
URL: r.URL,
|
|
}
|
|
}
|
|
|
|
// AppendTags -- appends key/val to ReqInfo.tags.
|
|
func (r *ReqInfo) AppendTags(key string, val string) *ReqInfo {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
r.tags = append(r.tags, KeyVal{key, val})
|
|
return r
|
|
}
|
|
|
|
// SetTags -- sets key/val to ReqInfo.tags.
|
|
func (r *ReqInfo) SetTags(key string, val string) *ReqInfo {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
r.Lock()
|
|
defer r.Unlock()
|
|
// Search for a tag key already existing in tags
|
|
var updated bool
|
|
for _, tag := range r.tags {
|
|
if tag.Key == key {
|
|
tag.Val = val
|
|
updated = true
|
|
break
|
|
}
|
|
}
|
|
if !updated {
|
|
// Append to the end of tags list
|
|
r.tags = append(r.tags, KeyVal{key, val})
|
|
}
|
|
return r
|
|
}
|
|
|
|
// GetTags -- returns the user defined tags.
|
|
func (r *ReqInfo) GetTags() []KeyVal {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
r.RLock()
|
|
defer r.RUnlock()
|
|
return append([]KeyVal(nil), r.tags...)
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
}
|
|
|
|
// SetReqInfo sets ReqInfo in the context.
|
|
func SetReqInfo(ctx context.Context, req *ReqInfo) context.Context {
|
|
if ctx == nil {
|
|
return nil
|
|
}
|
|
return context.WithValue(ctx, ctxRequestInfo, req)
|
|
}
|
|
|
|
// GetReqInfo returns ReqInfo if set.
|
|
// If ReqInfo isn't set returns new empty ReqInfo.
|
|
func GetReqInfo(ctx context.Context) *ReqInfo {
|
|
if ctx == nil {
|
|
return &ReqInfo{}
|
|
} else if r, ok := ctx.Value(ctxRequestInfo).(*ReqInfo); ok {
|
|
return r
|
|
}
|
|
return &ReqInfo{}
|
|
}
|
|
|
|
// SetReqLogger sets child zap.Logger in the context.
|
|
func SetReqLogger(ctx context.Context, log *zap.Logger) context.Context {
|
|
if ctx == nil {
|
|
return nil
|
|
}
|
|
return context.WithValue(ctx, ctxRequestLogger, log)
|
|
}
|
|
|
|
// GetReqLog returns log if set.
|
|
// If zap.Logger isn't set returns nil.
|
|
func GetReqLog(ctx context.Context) *zap.Logger {
|
|
if ctx == nil {
|
|
return nil
|
|
} else if r, ok := ctx.Value(ctxRequestLogger).(*zap.Logger); ok {
|
|
return r
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type RequestSettings interface {
|
|
NamespaceHeader() string
|
|
ResolveNamespaceAlias(string) string
|
|
}
|
|
|
|
func Request(log *zap.Logger, settings RequestSettings) Func {
|
|
return func(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
|
|
// also we have to set request id here
|
|
// to be able to get it in NewReqInfo
|
|
w.Header().Set(HdrAmzRequestID, id.String())
|
|
|
|
// set request info into context
|
|
// bucket name and object will be set in reqInfo later (limitation of go-chi)
|
|
reqInfo := NewReqInfo(w, r, ObjectRequest{})
|
|
reqInfo.Namespace = settings.ResolveNamespaceAlias(r.Header.Get(settings.NamespaceHeader()))
|
|
r = r.WithContext(SetReqInfo(r.Context(), reqInfo))
|
|
|
|
// set request id into gRPC meta header
|
|
r = r.WithContext(metadata.AppendToOutgoingContext(
|
|
r.Context(), HdrAmzRequestID, reqInfo.RequestID,
|
|
))
|
|
|
|
r = r.WithContext(treepool.SetRequestID(r.Context(), reqInfo.RequestID))
|
|
|
|
reqLogger := log.With(zap.String("request_id", reqInfo.RequestID))
|
|
r = r.WithContext(SetReqLogger(r.Context(), reqLogger))
|
|
|
|
reqLogger.Info(logs.RequestStart, zap.String("host", r.Host),
|
|
zap.String("remote_host", reqInfo.RemoteHost), zap.String("namespace", reqInfo.Namespace))
|
|
|
|
// continue execution
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// AddBucketName adds bucket name to ReqInfo from context.
|
|
func AddBucketName(l *zap.Logger) Func {
|
|
return func(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
reqInfo := GetReqInfo(ctx)
|
|
reqInfo.BucketName = chi.URLParam(r, BucketURLPrm)
|
|
|
|
reqLogger := reqLogOrDefault(ctx, l)
|
|
r = r.WithContext(SetReqLogger(ctx, reqLogger.With(zap.String("bucket", reqInfo.BucketName))))
|
|
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// AddObjectName adds objects name to ReqInfo from context.
|
|
func AddObjectName(l *zap.Logger) Func {
|
|
return func(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
reqInfo := GetReqInfo(ctx)
|
|
reqLogger := reqLogOrDefault(ctx, l)
|
|
|
|
rctx := chi.RouteContext(ctx)
|
|
// trim leading slash (always present)
|
|
reqInfo.ObjectName = rctx.RoutePath[1:]
|
|
|
|
if r.URL.RawPath != "" {
|
|
// we have to do this because of
|
|
// https://github.com/go-chi/chi/issues/641
|
|
// https://github.com/go-chi/chi/issues/642
|
|
if obj, err := url.PathUnescape(reqInfo.ObjectName); err != nil {
|
|
reqLogger.Warn(logs.FailedToUnescapeObjectName, zap.Error(err))
|
|
} else {
|
|
reqInfo.ObjectName = obj
|
|
}
|
|
}
|
|
|
|
r = r.WithContext(SetReqLogger(ctx, reqLogger.With(zap.String("object", reqInfo.ObjectName))))
|
|
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// getSourceIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239
|
|
// Forwarded headers (in that order), falls back to r.RemoteAddr when everything
|
|
// else fails.
|
|
func getSourceIP(r *http.Request) string {
|
|
var addr string
|
|
|
|
if fwd := r.Header.Get(xForwardedFor); fwd != "" {
|
|
// Only grabs the first (client) address. Note that '192.168.0.1,
|
|
// 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
|
|
// the first one may represent forwarding proxies earlier in the chain.
|
|
s := strings.Index(fwd, ", ")
|
|
if s == -1 {
|
|
s = len(fwd)
|
|
}
|
|
addr = fwd[:s]
|
|
} else if fwd := r.Header.Get(xRealIP); fwd != "" {
|
|
// X-Real-IP should only contain one IP address (the client making the
|
|
// request).
|
|
addr = fwd
|
|
} else if fwd := r.Header.Get(forwarded); fwd != "" {
|
|
// match should contain at least two elements if the protocol was
|
|
// specified in the Forwarded header. The first element will always be
|
|
// the 'for=' capture, which we ignore. In the case of multiple IP
|
|
// addresses (for=8.8.8.8, 8.8.4.4, 172.16.1.20 is valid) we only
|
|
// extract the first, which should be the client IP.
|
|
if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 {
|
|
// IPv6 addresses in Forwarded headers are quoted-strings. We strip
|
|
// these quotes.
|
|
addr = data.UnQuote(match[1])
|
|
}
|
|
}
|
|
|
|
if addr != "" {
|
|
return addr
|
|
}
|
|
|
|
// Default to remote address if headers not set.
|
|
addr, _, _ = net.SplitHostPort(r.RemoteAddr)
|
|
return addr
|
|
}
|