github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/generic-handlers.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"fmt"
    22  	"net"
    23  	"net/http"
    24  	"path"
    25  	"path/filepath"
    26  	"runtime/debug"
    27  	"strings"
    28  	"sync/atomic"
    29  	"time"
    30  
    31  	"github.com/dustin/go-humanize"
    32  	"github.com/minio/minio-go/v7/pkg/s3utils"
    33  	"github.com/minio/minio-go/v7/pkg/set"
    34  	"github.com/minio/minio/internal/grid"
    35  	xnet "github.com/minio/pkg/v2/net"
    36  	"golang.org/x/exp/maps"
    37  	"golang.org/x/exp/slices"
    38  
    39  	"github.com/minio/minio/internal/amztime"
    40  	"github.com/minio/minio/internal/config/dns"
    41  	"github.com/minio/minio/internal/crypto"
    42  	xhttp "github.com/minio/minio/internal/http"
    43  	"github.com/minio/minio/internal/logger"
    44  	"github.com/minio/minio/internal/mcontext"
    45  )
    46  
    47  const (
    48  	// Maximum allowed form data field values. 64MiB is a guessed practical value
    49  	// which is more than enough to accommodate any form data fields and headers.
    50  	requestFormDataSize = 64 * humanize.MiByte
    51  
    52  	// For any HTTP request, request body should be not more than 16GiB + requestFormDataSize
    53  	// where, 16GiB is the maximum allowed object size for object upload.
    54  	requestMaxBodySize = globalMaxObjectSize + requestFormDataSize
    55  
    56  	// Maximum size for http headers - See: https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html
    57  	maxHeaderSize = 8 * 1024
    58  
    59  	// Maximum size for user-defined metadata - See: https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html
    60  	maxUserDataSize = 2 * 1024
    61  
    62  	// maxBuckets upto 500000 for any MinIO deployment.
    63  	maxBuckets = 500 * 1000
    64  )
    65  
    66  // ReservedMetadataPrefix is the prefix of a metadata key which
    67  // is reserved and for internal use only.
    68  const (
    69  	ReservedMetadataPrefix      = "X-Minio-Internal-"
    70  	ReservedMetadataPrefixLower = "x-minio-internal-"
    71  )
    72  
    73  // containsReservedMetadata returns true if the http.Header contains
    74  // keys which are treated as metadata but are reserved for internal use
    75  // and must not set by clients
    76  func containsReservedMetadata(header http.Header) bool {
    77  	for key := range header {
    78  		if slices.Contains(maps.Keys(validSSEReplicationHeaders), key) {
    79  			return false
    80  		}
    81  		if stringsHasPrefixFold(key, ReservedMetadataPrefix) {
    82  			return true
    83  		}
    84  	}
    85  	return false
    86  }
    87  
    88  // isHTTPHeaderSizeTooLarge returns true if the provided
    89  // header is larger than 8 KB or the user-defined metadata
    90  // is larger than 2 KB.
    91  func isHTTPHeaderSizeTooLarge(header http.Header) bool {
    92  	var size, usersize int
    93  	for key := range header {
    94  		length := len(key) + len(header.Get(key))
    95  		size += length
    96  		for _, prefix := range userMetadataKeyPrefixes {
    97  			if stringsHasPrefixFold(key, prefix) {
    98  				usersize += length
    99  				break
   100  			}
   101  		}
   102  		if usersize > maxUserDataSize || size > maxHeaderSize {
   103  			return true
   104  		}
   105  	}
   106  	return false
   107  }
   108  
   109  // Limits body and header to specific allowed maximum limits as per S3/MinIO API requirements.
   110  func setRequestLimitMiddleware(h http.Handler) http.Handler {
   111  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   112  		tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
   113  
   114  		// Reject unsupported reserved metadata first before validation.
   115  		if containsReservedMetadata(r.Header) {
   116  			if ok {
   117  				tc.FuncName = "handler.ValidRequest"
   118  				tc.ResponseRecorder.LogErrBody = true
   119  			}
   120  
   121  			defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   122  			writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrUnsupportedMetadata), r.URL)
   123  			return
   124  		}
   125  
   126  		if isHTTPHeaderSizeTooLarge(r.Header) {
   127  			if ok {
   128  				tc.FuncName = "handler.ValidRequest"
   129  				tc.ResponseRecorder.LogErrBody = true
   130  			}
   131  
   132  			defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   133  			writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrMetadataTooLarge), r.URL)
   134  			atomic.AddUint64(&globalHTTPStats.rejectedRequestsHeader, 1)
   135  			return
   136  		}
   137  		// Restricting read data to a given maximum length
   138  		r.Body = http.MaxBytesReader(w, r.Body, requestMaxBodySize)
   139  		h.ServeHTTP(w, r)
   140  	})
   141  }
   142  
   143  // Reserved bucket.
   144  const (
   145  	minioReservedBucket     = "minio"
   146  	minioReservedBucketPath = SlashSeparator + minioReservedBucket
   147  
   148  	loginPathPrefix = SlashSeparator + "login"
   149  )
   150  
   151  func guessIsBrowserReq(r *http.Request) bool {
   152  	aType := getRequestAuthType(r)
   153  	return strings.Contains(r.Header.Get("User-Agent"), "Mozilla") &&
   154  		globalBrowserEnabled && aType == authTypeAnonymous
   155  }
   156  
   157  func setBrowserRedirectMiddleware(h http.Handler) http.Handler {
   158  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   159  		read := r.Method == http.MethodGet || r.Method == http.MethodHead
   160  		// Re-direction is handled specifically for browser requests.
   161  		if !guessIsHealthCheckReq(r) && guessIsBrowserReq(r) && read && globalBrowserRedirect {
   162  			// Fetch the redirect location if any.
   163  			if u := getRedirectLocation(r); u != nil {
   164  				// Employ a temporary re-direct.
   165  				http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)
   166  				return
   167  			}
   168  		}
   169  		h.ServeHTTP(w, r)
   170  	})
   171  }
   172  
   173  var redirectPrefixes = map[string]struct{}{
   174  	"favicon-16x16.png": {},
   175  	"favicon-32x32.png": {},
   176  	"favicon-96x96.png": {},
   177  	"index.html":        {},
   178  	minioReservedBucket: {},
   179  }
   180  
   181  // Fetch redirect location if urlPath satisfies certain
   182  // criteria. Some special names are considered to be
   183  // redirectable, this is purely internal function and
   184  // serves only limited purpose on redirect-handler for
   185  // browser requests.
   186  func getRedirectLocation(r *http.Request) *xnet.URL {
   187  	resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
   188  	if err != nil {
   189  		return nil
   190  	}
   191  	bucket, _ := path2BucketObject(resource)
   192  	_, redirect := redirectPrefixes[path.Clean(bucket)]
   193  	if redirect || resource == slashSeparator {
   194  		if globalBrowserRedirectURL != nil {
   195  			return globalBrowserRedirectURL
   196  		}
   197  		xhost, err := xnet.ParseHost(r.Host)
   198  		if err != nil {
   199  			return nil
   200  		}
   201  		return &xnet.URL{
   202  			Host: net.JoinHostPort(xhost.Name, globalMinioConsolePort),
   203  			Scheme: func() string {
   204  				scheme := "http"
   205  				if r.TLS != nil {
   206  					scheme = "https"
   207  				}
   208  				return scheme
   209  			}(),
   210  		}
   211  	}
   212  	return nil
   213  }
   214  
   215  // guessIsHealthCheckReq - returns true if incoming request looks
   216  // like healthCheck request
   217  func guessIsHealthCheckReq(req *http.Request) bool {
   218  	if req == nil {
   219  		return false
   220  	}
   221  	aType := getRequestAuthType(req)
   222  	return aType == authTypeAnonymous && (req.Method == http.MethodGet || req.Method == http.MethodHead) &&
   223  		(req.URL.Path == healthCheckPathPrefix+healthCheckLivenessPath ||
   224  			req.URL.Path == healthCheckPathPrefix+healthCheckReadinessPath ||
   225  			req.URL.Path == healthCheckPathPrefix+healthCheckClusterPath ||
   226  			req.URL.Path == healthCheckPathPrefix+healthCheckClusterReadPath)
   227  }
   228  
   229  // guessIsMetricsReq - returns true if incoming request looks
   230  // like metrics request
   231  func guessIsMetricsReq(req *http.Request) bool {
   232  	if req == nil {
   233  		return false
   234  	}
   235  	aType := getRequestAuthType(req)
   236  	return (aType == authTypeAnonymous || aType == authTypeJWT) &&
   237  		req.URL.Path == minioReservedBucketPath+prometheusMetricsPathLegacy ||
   238  		req.URL.Path == minioReservedBucketPath+prometheusMetricsV2ClusterPath ||
   239  		req.URL.Path == minioReservedBucketPath+prometheusMetricsV2NodePath ||
   240  		req.URL.Path == minioReservedBucketPath+prometheusMetricsV2BucketPath ||
   241  		req.URL.Path == minioReservedBucketPath+prometheusMetricsV2ResourcePath ||
   242  		strings.HasPrefix(req.URL.Path, minioReservedBucketPath+metricsV3Path)
   243  }
   244  
   245  // guessIsRPCReq - returns true if the request is for an RPC endpoint.
   246  func guessIsRPCReq(req *http.Request) bool {
   247  	if req == nil {
   248  		return false
   249  	}
   250  	if req.Method == http.MethodGet && req.URL != nil && req.URL.Path == grid.RoutePath {
   251  		return true
   252  	}
   253  
   254  	return req.Method == http.MethodPost &&
   255  		strings.HasPrefix(req.URL.Path, minioReservedBucketPath+SlashSeparator)
   256  }
   257  
   258  // Check to allow access to the reserved "bucket" `/minio` for Admin
   259  // API requests.
   260  func isAdminReq(r *http.Request) bool {
   261  	return strings.HasPrefix(r.URL.Path, adminPathPrefix)
   262  }
   263  
   264  // Check to allow access to the reserved "bucket" `/minio` for KMS
   265  // API requests.
   266  func isKMSReq(r *http.Request) bool {
   267  	return strings.HasPrefix(r.URL.Path, kmsPathPrefix)
   268  }
   269  
   270  // Supported Amz date headers.
   271  var amzDateHeaders = []string{
   272  	// Do not change this order, x-amz-date value should be
   273  	// validated first.
   274  	"x-amz-date",
   275  	"date",
   276  }
   277  
   278  // parseAmzDateHeader - parses supported amz date headers, in
   279  // supported amz date formats.
   280  func parseAmzDateHeader(req *http.Request) (time.Time, APIErrorCode) {
   281  	for _, amzDateHeader := range amzDateHeaders {
   282  		amzDateStr := req.Header.Get(amzDateHeader)
   283  		if amzDateStr != "" {
   284  			t, err := amztime.Parse(amzDateStr)
   285  			if err != nil {
   286  				return time.Time{}, ErrMalformedDate
   287  			}
   288  			return t, ErrNone
   289  		}
   290  	}
   291  	// Date header missing.
   292  	return time.Time{}, ErrMissingDateHeader
   293  }
   294  
   295  // Bad path components to be rejected by the path validity handler.
   296  const (
   297  	dotdotComponent = ".."
   298  	dotComponent    = "."
   299  )
   300  
   301  func hasBadHost(host string) error {
   302  	if globalIsCICD && strings.TrimSpace(host) == "" {
   303  		// under CI/CD test setups ignore empty hosts as invalid hosts
   304  		return nil
   305  	}
   306  	_, err := xnet.ParseHost(host)
   307  	return err
   308  }
   309  
   310  // Check if the incoming path has bad path components,
   311  // such as ".." and "."
   312  func hasBadPathComponent(path string) bool {
   313  	path = filepath.ToSlash(strings.TrimSpace(path)) // For windows '\' must be converted to '/'
   314  	for _, p := range strings.Split(path, SlashSeparator) {
   315  		switch strings.TrimSpace(p) {
   316  		case dotdotComponent:
   317  			return true
   318  		case dotComponent:
   319  			return true
   320  		}
   321  	}
   322  	return false
   323  }
   324  
   325  // Check if client is sending a malicious request.
   326  func hasMultipleAuth(r *http.Request) bool {
   327  	authTypeCount := 0
   328  	for _, hasValidAuth := range []func(*http.Request) bool{
   329  		isRequestSignatureV2, isRequestPresignedSignatureV2,
   330  		isRequestSignatureV4, isRequestPresignedSignatureV4,
   331  		isRequestJWT, isRequestPostPolicySignatureV4,
   332  	} {
   333  		if hasValidAuth(r) {
   334  			authTypeCount++
   335  		}
   336  	}
   337  	return authTypeCount > 1
   338  }
   339  
   340  // requestValidityHandler validates all the incoming paths for
   341  // any malicious requests.
   342  func setRequestValidityMiddleware(h http.Handler) http.Handler {
   343  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   344  		tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
   345  
   346  		if err := hasBadHost(r.Host); err != nil {
   347  			if ok {
   348  				tc.FuncName = "handler.ValidRequest"
   349  				tc.ResponseRecorder.LogErrBody = true
   350  			}
   351  
   352  			defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   353  			invalidReq := errorCodes.ToAPIErr(ErrInvalidRequest)
   354  			invalidReq.Description = fmt.Sprintf("%s (%s)", invalidReq.Description, err)
   355  			writeErrorResponse(r.Context(), w, invalidReq, r.URL)
   356  			atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
   357  			return
   358  		}
   359  
   360  		// Check for bad components in URL path.
   361  		if hasBadPathComponent(r.URL.Path) {
   362  			if ok {
   363  				tc.FuncName = "handler.ValidRequest"
   364  				tc.ResponseRecorder.LogErrBody = true
   365  			}
   366  
   367  			defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   368  			writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL)
   369  			atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
   370  			return
   371  		}
   372  		// Check for bad components in URL query values.
   373  		for k, vv := range r.Form {
   374  			if k == "delimiter" { // delimiters are allowed to have `.` or `..`
   375  				continue
   376  			}
   377  			for _, v := range vv {
   378  				if hasBadPathComponent(v) {
   379  					if ok {
   380  						tc.FuncName = "handler.ValidRequest"
   381  						tc.ResponseRecorder.LogErrBody = true
   382  					}
   383  
   384  					defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   385  					writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL)
   386  					atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
   387  					return
   388  				}
   389  			}
   390  		}
   391  		if hasMultipleAuth(r) {
   392  			if ok {
   393  				tc.FuncName = "handler.Auth"
   394  				tc.ResponseRecorder.LogErrBody = true
   395  			}
   396  
   397  			defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   398  			invalidReq := errorCodes.ToAPIErr(ErrInvalidRequest)
   399  			invalidReq.Description = fmt.Sprintf("%s (request has multiple authentication types, please use one)", invalidReq.Description)
   400  			writeErrorResponse(r.Context(), w, invalidReq, r.URL)
   401  			atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
   402  			return
   403  		}
   404  		// For all other requests reject access to reserved buckets
   405  		bucketName, _ := request2BucketObjectName(r)
   406  		if isMinioReservedBucket(bucketName) || isMinioMetaBucket(bucketName) {
   407  			if !guessIsRPCReq(r) && !guessIsBrowserReq(r) && !guessIsHealthCheckReq(r) && !guessIsMetricsReq(r) && !isAdminReq(r) && !isKMSReq(r) {
   408  				if ok {
   409  					tc.FuncName = "handler.ValidRequest"
   410  					tc.ResponseRecorder.LogErrBody = true
   411  				}
   412  				defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   413  				writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrAllAccessDisabled), r.URL)
   414  				return
   415  			}
   416  		} else {
   417  			// Validate bucket names if it is not empty
   418  			if bucketName != "" && s3utils.CheckValidBucketNameStrict(bucketName) != nil {
   419  				if ok {
   420  					tc.FuncName = "handler.ValidRequest"
   421  					tc.ResponseRecorder.LogErrBody = true
   422  				}
   423  				defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   424  				writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidBucketName), r.URL)
   425  				return
   426  			}
   427  		}
   428  		// Deny SSE-C requests if not made over TLS
   429  		if !globalIsTLS && (crypto.SSEC.IsRequested(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
   430  			if r.Method == http.MethodHead {
   431  				if ok {
   432  					tc.FuncName = "handler.ValidRequest"
   433  					tc.ResponseRecorder.LogErrBody = false
   434  				}
   435  
   436  				defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   437  				writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrInsecureSSECustomerRequest))
   438  			} else {
   439  				if ok {
   440  					tc.FuncName = "handler.ValidRequest"
   441  					tc.ResponseRecorder.LogErrBody = true
   442  				}
   443  
   444  				defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   445  				writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInsecureSSECustomerRequest), r.URL)
   446  			}
   447  			return
   448  		}
   449  		h.ServeHTTP(w, r)
   450  	})
   451  }
   452  
   453  // setBucketForwardingMiddleware middleware forwards the path style requests
   454  // on a bucket to the right bucket location, bucket to IP configuration
   455  // is obtained from centralized etcd configuration service.
   456  func setBucketForwardingMiddleware(h http.Handler) http.Handler {
   457  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   458  		if origin := w.Header().Get("Access-Control-Allow-Origin"); origin == "null" {
   459  			// This is a workaround change to ensure that "Origin: null"
   460  			// incoming request to a response back as "*" instead of "null"
   461  			w.Header().Set("Access-Control-Allow-Origin", "*")
   462  		}
   463  		if globalDNSConfig == nil || !globalBucketFederation ||
   464  			guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
   465  			guessIsRPCReq(r) || guessIsLoginSTSReq(r) || isAdminReq(r) {
   466  			h.ServeHTTP(w, r)
   467  			return
   468  		}
   469  
   470  		bucket, object := request2BucketObjectName(r)
   471  
   472  		// Requests in federated setups for STS type calls which are
   473  		// performed at '/' resource should be routed by the muxer,
   474  		// the assumption is simply such that requests without a bucket
   475  		// in a federated setup cannot be proxied, so serve them at
   476  		// current server.
   477  		if bucket == "" {
   478  			h.ServeHTTP(w, r)
   479  			return
   480  		}
   481  
   482  		// MakeBucket requests should be handled at current endpoint
   483  		if r.Method == http.MethodPut && bucket != "" && object == "" && r.URL.RawQuery == "" {
   484  			h.ServeHTTP(w, r)
   485  			return
   486  		}
   487  
   488  		// CopyObject requests should be handled at current endpoint as path style
   489  		// requests have target bucket and object in URI and source details are in
   490  		// header fields
   491  		if r.Method == http.MethodPut && r.Header.Get(xhttp.AmzCopySource) != "" {
   492  			bucket, object = path2BucketObject(r.Header.Get(xhttp.AmzCopySource))
   493  			if bucket == "" || object == "" {
   494  				h.ServeHTTP(w, r)
   495  				return
   496  			}
   497  		}
   498  		sr, err := globalDNSConfig.Get(bucket)
   499  		if err != nil {
   500  			defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   501  			if err == dns.ErrNoEntriesFound {
   502  				writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrNoSuchBucket), r.URL)
   503  			} else {
   504  				writeErrorResponse(r.Context(), w, toAPIError(r.Context(), err), r.URL)
   505  			}
   506  			return
   507  		}
   508  		if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
   509  			r.URL.Scheme = "http"
   510  			if globalIsTLS {
   511  				r.URL.Scheme = "https"
   512  			}
   513  			r.URL.Host = getHostFromSrv(sr)
   514  			// Make sure we remove any existing headers before
   515  			// proxying the request to another node.
   516  			for k := range w.Header() {
   517  				w.Header().Del(k)
   518  			}
   519  			globalForwarder.ServeHTTP(w, r)
   520  			return
   521  		}
   522  		h.ServeHTTP(w, r)
   523  	})
   524  }
   525  
   526  // addCustomHeadersMiddleware adds various HTTP(S) response headers.
   527  // Security Headers enable various security protections behaviors in the client's browser.
   528  func addCustomHeadersMiddleware(h http.Handler) http.Handler {
   529  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   530  		header := w.Header()
   531  		header.Set("X-XSS-Protection", "1; mode=block")                                // Prevents against XSS attacks
   532  		header.Set("X-Content-Type-Options", "nosniff")                                // Prevent mime-sniff
   533  		header.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") // HSTS mitigates variants of MITM attacks
   534  
   535  		// Previously, this value was set right before a response was sent to
   536  		// the client. So, logger and Error response XML were not using this
   537  		// value. This is set here so that this header can be logged as
   538  		// part of the log entry, Error response XML and auditing.
   539  		// Set custom headers such as x-amz-request-id for each request.
   540  		w.Header().Set(xhttp.AmzRequestID, mustGetRequestID(UTCNow()))
   541  		if globalLocalNodeName != "" {
   542  			w.Header().Set(xhttp.AmzRequestHostID, globalLocalNodeNameHex)
   543  		}
   544  		h.ServeHTTP(w, r)
   545  	})
   546  }
   547  
   548  // criticalErrorHandler handles panics and fatal errors by
   549  // `panic(logger.ErrCritical)` as done by `logger.CriticalIf`.
   550  //
   551  // It should be always the first / highest HTTP handler.
   552  func setCriticalErrorHandler(h http.Handler) http.Handler {
   553  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   554  		defer func() {
   555  			if rec := recover(); rec == logger.ErrCritical { // handle
   556  				stack := debug.Stack()
   557  				logger.Error("critical: \"%s %s\": %v\n%s", r.Method, r.URL, rec, string(stack))
   558  				writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
   559  				return
   560  			} else if rec != nil {
   561  				stack := debug.Stack()
   562  				logger.Error("panic: \"%s %s\": %v\n%s", r.Method, r.URL, rec, string(stack))
   563  				// Try to write an error response, upstream may not have written header.
   564  				writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
   565  				return
   566  			}
   567  		}()
   568  		h.ServeHTTP(w, r)
   569  	})
   570  }
   571  
   572  // setUploadForwardingMiddleware middleware forwards multiparts requests
   573  // in a site replication setup to peer that initiated the upload
   574  func setUploadForwardingMiddleware(h http.Handler) http.Handler {
   575  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   576  		if !globalSiteReplicationSys.isEnabled() ||
   577  			guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
   578  			guessIsRPCReq(r) || guessIsLoginSTSReq(r) || isAdminReq(r) {
   579  			h.ServeHTTP(w, r)
   580  			return
   581  		}
   582  
   583  		bucket, object := request2BucketObjectName(r)
   584  		uploadID := r.Form.Get(xhttp.UploadID)
   585  
   586  		if bucket != "" && object != "" && uploadID != "" {
   587  			deplID, err := getDeplIDFromUpload(uploadID)
   588  			if err != nil {
   589  				h.ServeHTTP(w, r)
   590  				return
   591  			}
   592  			remote, self := globalSiteReplicationSys.getPeerForUpload(deplID)
   593  			if self {
   594  				h.ServeHTTP(w, r)
   595  				return
   596  			}
   597  			// forward request to peer handling this upload
   598  			if globalBucketTargetSys.isOffline(remote.EndpointURL) {
   599  				defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   600  				writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrReplicationRemoteConnectionError), r.URL)
   601  				return
   602  			}
   603  
   604  			r.URL.Scheme = remote.EndpointURL.Scheme
   605  			r.URL.Host = remote.EndpointURL.Host
   606  			// Make sure we remove any existing headers before
   607  			// proxying the request to another node.
   608  			for k := range w.Header() {
   609  				w.Header().Del(k)
   610  			}
   611  			ctx := newContext(r, w, "SiteReplicationUploadForwarding")
   612  			defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
   613  			globalForwarder.ServeHTTP(w, r)
   614  			return
   615  		}
   616  		h.ServeHTTP(w, r)
   617  	})
   618  }