github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/handler-utils.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  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"net/http"
    25  	"net/textproto"
    26  	"regexp"
    27  	"strings"
    28  
    29  	"github.com/minio/madmin-go/v3"
    30  	"github.com/minio/minio/internal/auth"
    31  	"github.com/minio/minio/internal/handlers"
    32  	xhttp "github.com/minio/minio/internal/http"
    33  	"github.com/minio/minio/internal/logger"
    34  	"github.com/minio/minio/internal/mcontext"
    35  	xnet "github.com/minio/pkg/v2/net"
    36  	"golang.org/x/exp/maps"
    37  	"golang.org/x/exp/slices"
    38  )
    39  
    40  const (
    41  	copyDirective    = "COPY"
    42  	replaceDirective = "REPLACE"
    43  	accessDirective  = "ACCESS"
    44  )
    45  
    46  // Parses location constraint from the incoming reader.
    47  func parseLocationConstraint(r *http.Request) (location string, s3Error APIErrorCode) {
    48  	// If the request has no body with content-length set to 0,
    49  	// we do not have to validate location constraint. Bucket will
    50  	// be created at default region.
    51  	locationConstraint := createBucketLocationConfiguration{}
    52  	err := xmlDecoder(r.Body, &locationConstraint, r.ContentLength)
    53  	if err != nil && r.ContentLength != 0 {
    54  		logger.LogOnceIf(GlobalContext, err, "location-constraint-xml-parsing")
    55  		// Treat all other failures as XML parsing errors.
    56  		return "", ErrMalformedXML
    57  	} // else for both err as nil or io.EOF
    58  	location = locationConstraint.Location
    59  	if location == "" {
    60  		location = globalSite.Region
    61  	}
    62  	if !isValidLocation(location) {
    63  		return location, ErrInvalidRegion
    64  	}
    65  
    66  	return location, ErrNone
    67  }
    68  
    69  // Validates input location is same as configured region
    70  // of MinIO server.
    71  func isValidLocation(location string) bool {
    72  	return globalSite.Region == "" || globalSite.Region == location
    73  }
    74  
    75  // Supported headers that needs to be extracted.
    76  var supportedHeaders = []string{
    77  	"content-type",
    78  	"cache-control",
    79  	"content-language",
    80  	"content-encoding",
    81  	"content-disposition",
    82  	"x-amz-storage-class",
    83  	xhttp.AmzStorageClass,
    84  	xhttp.AmzObjectTagging,
    85  	"expires",
    86  	xhttp.AmzBucketReplicationStatus,
    87  	"X-Minio-Replication-Server-Side-Encryption-Sealed-Key",
    88  	"X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm",
    89  	"X-Minio-Replication-Server-Side-Encryption-Iv",
    90  	"X-Minio-Replication-Encrypted-Multipart",
    91  	"X-Minio-Replication-Actual-Object-Size",
    92  	// Add more supported headers here.
    93  }
    94  
    95  // mapping of internal headers to allowed replication headers
    96  var validSSEReplicationHeaders = map[string]string{
    97  	"X-Minio-Internal-Server-Side-Encryption-Sealed-Key":     "X-Minio-Replication-Server-Side-Encryption-Sealed-Key",
    98  	"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm",
    99  	"X-Minio-Internal-Server-Side-Encryption-Iv":             "X-Minio-Replication-Server-Side-Encryption-Iv",
   100  	"X-Minio-Internal-Encrypted-Multipart":                   "X-Minio-Replication-Encrypted-Multipart",
   101  	"X-Minio-Internal-Actual-Object-Size":                    "X-Minio-Replication-Actual-Object-Size",
   102  	// Add more supported headers here.
   103  }
   104  
   105  // mapping of replication headers to internal headers
   106  var replicationToInternalHeaders = map[string]string{
   107  	"X-Minio-Replication-Server-Side-Encryption-Sealed-Key":     "X-Minio-Internal-Server-Side-Encryption-Sealed-Key",
   108  	"X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm": "X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm",
   109  	"X-Minio-Replication-Server-Side-Encryption-Iv":             "X-Minio-Internal-Server-Side-Encryption-Iv",
   110  	"X-Minio-Replication-Encrypted-Multipart":                   "X-Minio-Internal-Encrypted-Multipart",
   111  	"X-Minio-Replication-Actual-Object-Size":                    "X-Minio-Internal-Actual-Object-Size",
   112  	// Add more supported headers here.
   113  }
   114  
   115  // isDirectiveValid - check if tagging-directive is valid.
   116  func isDirectiveValid(v string) bool {
   117  	// Check if set metadata-directive is valid.
   118  	return isDirectiveCopy(v) || isDirectiveReplace(v)
   119  }
   120  
   121  // Check if the directive COPY is requested.
   122  func isDirectiveCopy(value string) bool {
   123  	// By default if directive is not set we
   124  	// treat it as 'COPY' this function returns true.
   125  	return value == copyDirective || value == ""
   126  }
   127  
   128  // Check if the directive REPLACE is requested.
   129  func isDirectiveReplace(value string) bool {
   130  	return value == replaceDirective
   131  }
   132  
   133  // userMetadataKeyPrefixes contains the prefixes of used-defined metadata keys.
   134  // All values stored with a key starting with one of the following prefixes
   135  // must be extracted from the header.
   136  var userMetadataKeyPrefixes = []string{
   137  	"x-amz-meta-",
   138  	"x-minio-meta-",
   139  }
   140  
   141  // extractMetadataFromReq extracts metadata from HTTP header and HTTP queryString.
   142  func extractMetadataFromReq(ctx context.Context, r *http.Request) (metadata map[string]string, err error) {
   143  	return extractMetadata(ctx, textproto.MIMEHeader(r.Form), textproto.MIMEHeader(r.Header))
   144  }
   145  
   146  func extractMetadata(ctx context.Context, mimesHeader ...textproto.MIMEHeader) (metadata map[string]string, err error) {
   147  	metadata = make(map[string]string)
   148  
   149  	for _, hdr := range mimesHeader {
   150  		// Extract all query values.
   151  		err = extractMetadataFromMime(ctx, hdr, metadata)
   152  		if err != nil {
   153  			return nil, err
   154  		}
   155  	}
   156  
   157  	// Set content-type to default value if it is not set.
   158  	if _, ok := metadata[strings.ToLower(xhttp.ContentType)]; !ok {
   159  		metadata[strings.ToLower(xhttp.ContentType)] = "binary/octet-stream"
   160  	}
   161  
   162  	// https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w
   163  	for k := range metadata {
   164  		if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) {
   165  			delete(metadata, k)
   166  		}
   167  	}
   168  
   169  	if contentEncoding, ok := metadata[strings.ToLower(xhttp.ContentEncoding)]; ok {
   170  		contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
   171  		if contentEncoding != "" {
   172  			// Make sure to trim and save the content-encoding
   173  			// parameter for a streaming signature which is set
   174  			// to a custom value for example: "aws-chunked,gzip".
   175  			metadata[strings.ToLower(xhttp.ContentEncoding)] = contentEncoding
   176  		} else {
   177  			// Trimmed content encoding is empty when the header
   178  			// value is set to "aws-chunked" only.
   179  
   180  			// Make sure to delete the content-encoding parameter
   181  			// for a streaming signature which is set to value
   182  			// for example: "aws-chunked"
   183  			delete(metadata, strings.ToLower(xhttp.ContentEncoding))
   184  		}
   185  	}
   186  
   187  	// Success.
   188  	return metadata, nil
   189  }
   190  
   191  // extractMetadata extracts metadata from map values.
   192  func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[string]string) error {
   193  	if v == nil {
   194  		logger.LogIf(ctx, errInvalidArgument)
   195  		return errInvalidArgument
   196  	}
   197  
   198  	nv := make(textproto.MIMEHeader, len(v))
   199  	for k, kv := range v {
   200  		// Canonicalize all headers, to remove any duplicates.
   201  		nv[http.CanonicalHeaderKey(k)] = kv
   202  	}
   203  
   204  	// Save all supported headers.
   205  	for _, supportedHeader := range supportedHeaders {
   206  		value, ok := nv[http.CanonicalHeaderKey(supportedHeader)]
   207  		if ok {
   208  			if slices.Contains(maps.Keys(replicationToInternalHeaders), supportedHeader) {
   209  				m[replicationToInternalHeaders[supportedHeader]] = strings.Join(value, ",")
   210  			} else {
   211  				m[supportedHeader] = strings.Join(value, ",")
   212  			}
   213  		}
   214  	}
   215  
   216  	for key := range v {
   217  		for _, prefix := range userMetadataKeyPrefixes {
   218  			if !stringsHasPrefixFold(key, prefix) {
   219  				continue
   220  			}
   221  			value, ok := nv[http.CanonicalHeaderKey(key)]
   222  			if ok {
   223  				m[key] = strings.Join(value, ",")
   224  				break
   225  			}
   226  		}
   227  	}
   228  	return nil
   229  }
   230  
   231  // Returns access credentials in the request Authorization header.
   232  func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) {
   233  	cred, _, _ = getReqAccessKeyV4(r, region, serviceS3)
   234  	if cred.AccessKey == "" {
   235  		cred, _, _ = getReqAccessKeyV2(r)
   236  	}
   237  	return cred
   238  }
   239  
   240  // Extract request params to be sent with event notification.
   241  func extractReqParams(r *http.Request) map[string]string {
   242  	if r == nil {
   243  		return nil
   244  	}
   245  
   246  	region := globalSite.Region
   247  	cred := getReqAccessCred(r, region)
   248  
   249  	principalID := cred.AccessKey
   250  	if cred.ParentUser != "" {
   251  		principalID = cred.ParentUser
   252  	}
   253  
   254  	// Success.
   255  	m := map[string]string{
   256  		"region":          region,
   257  		"principalId":     principalID,
   258  		"sourceIPAddress": handlers.GetSourceIP(r),
   259  		// Add more fields here.
   260  	}
   261  	if rangeField := r.Header.Get(xhttp.Range); rangeField != "" {
   262  		m["range"] = rangeField
   263  	}
   264  
   265  	if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok {
   266  		m[xhttp.MinIOSourceReplicationRequest] = ""
   267  	}
   268  	return m
   269  }
   270  
   271  // Extract response elements to be sent with event notification.
   272  func extractRespElements(w http.ResponseWriter) map[string]string {
   273  	if w == nil {
   274  		return map[string]string{}
   275  	}
   276  	return map[string]string{
   277  		"requestId":      w.Header().Get(xhttp.AmzRequestID),
   278  		"nodeId":         w.Header().Get(xhttp.AmzRequestHostID),
   279  		"content-length": w.Header().Get(xhttp.ContentLength),
   280  		// Add more fields here.
   281  	}
   282  }
   283  
   284  // Trims away `aws-chunked` from the content-encoding header if present.
   285  // Streaming signature clients can have custom content-encoding such as
   286  // `aws-chunked,gzip` here we need to only save `gzip`.
   287  // For more refer http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
   288  func trimAwsChunkedContentEncoding(contentEnc string) (trimmedContentEnc string) {
   289  	if contentEnc == "" {
   290  		return contentEnc
   291  	}
   292  	var newEncs []string
   293  	for _, enc := range strings.Split(contentEnc, ",") {
   294  		if enc != streamingContentEncoding {
   295  			newEncs = append(newEncs, enc)
   296  		}
   297  	}
   298  	return strings.Join(newEncs, ",")
   299  }
   300  
   301  func collectInternodeStats(f http.HandlerFunc) http.HandlerFunc {
   302  	return func(w http.ResponseWriter, r *http.Request) {
   303  		f.ServeHTTP(w, r)
   304  
   305  		tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
   306  		if !ok || tc == nil {
   307  			return
   308  		}
   309  
   310  		globalConnStats.incInternodeInputBytes(int64(tc.RequestRecorder.Size()))
   311  		globalConnStats.incInternodeOutputBytes(int64(tc.ResponseRecorder.Size()))
   312  	}
   313  }
   314  
   315  func collectAPIStats(api string, f http.HandlerFunc) http.HandlerFunc {
   316  	return func(w http.ResponseWriter, r *http.Request) {
   317  		resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
   318  		if err != nil {
   319  			defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
   320  
   321  			apiErr := errorCodes.ToAPIErr(ErrUnsupportedHostHeader)
   322  			apiErr.Description = fmt.Sprintf("%s: %v", apiErr.Description, err)
   323  
   324  			writeErrorResponse(r.Context(), w, apiErr, r.URL)
   325  			return
   326  		}
   327  
   328  		bucket, _ := path2BucketObject(resource)
   329  
   330  		meta, err := globalBucketMetadataSys.Get(bucket) // check if this bucket exists.
   331  		countBktStat := bucket != "" && bucket != minioReservedBucket && err == nil && !meta.Created.IsZero()
   332  		if countBktStat {
   333  			globalBucketHTTPStats.updateHTTPStats(bucket, api, nil)
   334  		}
   335  
   336  		globalHTTPStats.currentS3Requests.Inc(api)
   337  		f.ServeHTTP(w, r)
   338  		globalHTTPStats.currentS3Requests.Dec(api)
   339  
   340  		tc, _ := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
   341  		if tc != nil {
   342  			globalHTTPStats.updateStats(api, tc.ResponseRecorder)
   343  			globalConnStats.incS3InputBytes(int64(tc.RequestRecorder.Size()))
   344  			globalConnStats.incS3OutputBytes(int64(tc.ResponseRecorder.Size()))
   345  
   346  			if countBktStat {
   347  				globalBucketConnStats.incS3InputBytes(bucket, int64(tc.RequestRecorder.Size()))
   348  				globalBucketConnStats.incS3OutputBytes(bucket, int64(tc.ResponseRecorder.Size()))
   349  				globalBucketHTTPStats.updateHTTPStats(bucket, api, tc.ResponseRecorder)
   350  			}
   351  		}
   352  	}
   353  }
   354  
   355  // Returns "/bucketName/objectName" for path-style or virtual-host-style requests.
   356  func getResource(path string, host string, domains []string) (string, error) {
   357  	if len(domains) == 0 {
   358  		return path, nil
   359  	}
   360  
   361  	// If virtual-host-style is enabled construct the "resource" properly.
   362  	xhost, err := xnet.ParseHost(host)
   363  	if err != nil {
   364  		return "", err
   365  	}
   366  
   367  	for _, domain := range domains {
   368  		if xhost.Name == minioReservedBucket+"."+domain {
   369  			continue
   370  		}
   371  		if !strings.HasSuffix(xhost.Name, "."+domain) {
   372  			continue
   373  		}
   374  		bucket := strings.TrimSuffix(xhost.Name, "."+domain)
   375  		return SlashSeparator + pathJoin(bucket, path), nil
   376  	}
   377  	return path, nil
   378  }
   379  
   380  var regexVersion = regexp.MustCompile(`^/minio.*/(v\d+)/.*`)
   381  
   382  func extractAPIVersion(r *http.Request) string {
   383  	if matches := regexVersion.FindStringSubmatch(r.URL.Path); len(matches) > 1 {
   384  		return matches[1]
   385  	}
   386  	return "unknown"
   387  }
   388  
   389  func methodNotAllowedHandler(api string) func(w http.ResponseWriter, r *http.Request) {
   390  	return errorResponseHandler
   391  }
   392  
   393  // If none of the http routes match respond with appropriate errors
   394  func errorResponseHandler(w http.ResponseWriter, r *http.Request) {
   395  	if r.Method == http.MethodOptions {
   396  		return
   397  	}
   398  	desc := "Do not upgrade one server at a time - please follow the recommended guidelines mentioned here https://github.com/minio/minio#upgrading-minio for your environment"
   399  	switch {
   400  	case strings.HasPrefix(r.URL.Path, peerRESTPrefix):
   401  		writeErrorResponseString(r.Context(), w, APIError{
   402  			Code:           "XMinioPeerVersionMismatch",
   403  			Description:    desc,
   404  			HTTPStatusCode: http.StatusUpgradeRequired,
   405  		}, r.URL)
   406  	case strings.HasPrefix(r.URL.Path, storageRESTPrefix):
   407  		writeErrorResponseString(r.Context(), w, APIError{
   408  			Code:           "XMinioStorageVersionMismatch",
   409  			Description:    desc,
   410  			HTTPStatusCode: http.StatusUpgradeRequired,
   411  		}, r.URL)
   412  	case strings.HasPrefix(r.URL.Path, adminPathPrefix):
   413  		var desc string
   414  		version := extractAPIVersion(r)
   415  		switch version {
   416  		case "v1", madmin.AdminAPIVersionV2:
   417  			desc = fmt.Sprintf("Server expects client requests with 'admin' API version '%s', found '%s', please upgrade the client to latest releases", madmin.AdminAPIVersion, version)
   418  		case madmin.AdminAPIVersion:
   419  			desc = fmt.Sprintf("This 'admin' API is not supported by server in '%s'", getMinioMode())
   420  		default:
   421  			desc = fmt.Sprintf("Unexpected client 'admin' API version found '%s', expected '%s', please downgrade the client to older releases", version, madmin.AdminAPIVersion)
   422  		}
   423  		writeErrorResponseJSON(r.Context(), w, APIError{
   424  			Code:           "XMinioAdminVersionMismatch",
   425  			Description:    desc,
   426  			HTTPStatusCode: http.StatusUpgradeRequired,
   427  		}, r.URL)
   428  	default:
   429  		writeErrorResponse(r.Context(), w, APIError{
   430  			Code: "BadRequest",
   431  			Description: fmt.Sprintf("An error occurred when parsing the HTTP request %s at '%s'",
   432  				r.Method, r.URL.Path),
   433  			HTTPStatusCode: http.StatusBadRequest,
   434  		}, r.URL)
   435  	}
   436  }
   437  
   438  // gets host name for current node
   439  func getHostName(r *http.Request) (hostName string) {
   440  	if globalIsDistErasure {
   441  		hostName = globalLocalNodeName
   442  	} else {
   443  		hostName = r.Host
   444  	}
   445  	return
   446  }
   447  
   448  // Proxy any request to an endpoint.
   449  func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ep ProxyEndpoint) (success bool) {
   450  	success = true
   451  
   452  	// Make sure we remove any existing headers before
   453  	// proxying the request to another node.
   454  	for k := range w.Header() {
   455  		w.Header().Del(k)
   456  	}
   457  
   458  	f := handlers.NewForwarder(&handlers.Forwarder{
   459  		PassHost:     true,
   460  		RoundTripper: ep.Transport,
   461  		ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
   462  			success = false
   463  			if err != nil && !errors.Is(err, context.Canceled) {
   464  				logger.LogIf(GlobalContext, err)
   465  			}
   466  		},
   467  	})
   468  
   469  	r.URL.Scheme = "http"
   470  	if globalIsTLS {
   471  		r.URL.Scheme = "https"
   472  	}
   473  
   474  	r.URL.Host = ep.Host
   475  	f.ServeHTTP(w, r)
   476  	return
   477  }