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 }