storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/handler-utils.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2015-2020 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package cmd 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "mime/multipart" 27 "net" 28 "net/http" 29 "net/textproto" 30 "net/url" 31 "regexp" 32 "strings" 33 34 xhttp "storj.io/minio/cmd/http" 35 "storj.io/minio/cmd/logger" 36 "storj.io/minio/pkg/auth" 37 "storj.io/minio/pkg/handlers" 38 "storj.io/minio/pkg/madmin" 39 ) 40 41 const ( 42 copyDirective = "COPY" 43 replaceDirective = "REPLACE" 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.LogIf(GlobalContext, err) 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 = globalServerRegion 61 } 62 return location, ErrNone 63 } 64 65 // Validates input location is same as configured region 66 // of MinIO server. 67 func isValidLocation(location string) bool { 68 return globalServerRegion == "" || globalServerRegion == location 69 } 70 71 // Supported headers that needs to be extracted. 72 var supportedHeaders = []string{ 73 "content-type", 74 "cache-control", 75 "content-language", 76 "content-encoding", 77 "content-disposition", 78 "x-amz-storage-class", 79 xhttp.AmzStorageClass, 80 xhttp.AmzObjectTagging, 81 "expires", 82 xhttp.AmzBucketReplicationStatus, 83 // Add more supported headers here. 84 } 85 86 // isDirectiveValid - check if tagging-directive is valid. 87 func isDirectiveValid(v string) bool { 88 // Check if set metadata-directive is valid. 89 return isDirectiveCopy(v) || isDirectiveReplace(v) 90 } 91 92 // Check if the directive COPY is requested. 93 func isDirectiveCopy(value string) bool { 94 // By default if directive is not set we 95 // treat it as 'COPY' this function returns true. 96 return value == copyDirective || value == "" 97 } 98 99 // Check if the directive REPLACE is requested. 100 func isDirectiveReplace(value string) bool { 101 return value == replaceDirective 102 } 103 104 // userMetadataKeyPrefixes contains the prefixes of used-defined metadata keys. 105 // All values stored with a key starting with one of the following prefixes 106 // must be extracted from the header. 107 var userMetadataKeyPrefixes = []string{ 108 "x-amz-meta-", 109 "x-minio-meta-", 110 } 111 112 // extractMetadata extracts metadata from HTTP header and HTTP queryString. 113 func extractMetadata(ctx context.Context, r *http.Request) (metadata map[string]string, err error) { 114 query := r.URL.Query() 115 header := r.Header 116 metadata = make(map[string]string) 117 // Extract all query values. 118 err = extractMetadataFromMime(ctx, textproto.MIMEHeader(query), metadata) 119 if err != nil { 120 return nil, err 121 } 122 123 // Extract all header values. 124 err = extractMetadataFromMime(ctx, textproto.MIMEHeader(header), metadata) 125 if err != nil { 126 return nil, err 127 } 128 129 // Set content-type to default value if it is not set. 130 if _, ok := metadata[strings.ToLower(xhttp.ContentType)]; !ok { 131 metadata[strings.ToLower(xhttp.ContentType)] = "binary/octet-stream" 132 } 133 134 // https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w 135 for k := range metadata { 136 if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) { 137 delete(metadata, k) 138 } 139 } 140 141 if contentEncoding, ok := metadata[strings.ToLower(xhttp.ContentEncoding)]; ok { 142 contentEncoding = trimAwsChunkedContentEncoding(contentEncoding) 143 if contentEncoding != "" { 144 // Make sure to trim and save the content-encoding 145 // parameter for a streaming signature which is set 146 // to a custom value for example: "aws-chunked,gzip". 147 metadata[strings.ToLower(xhttp.ContentEncoding)] = contentEncoding 148 } else { 149 // Trimmed content encoding is empty when the header 150 // value is set to "aws-chunked" only. 151 152 // Make sure to delete the content-encoding parameter 153 // for a streaming signature which is set to value 154 // for example: "aws-chunked" 155 delete(metadata, strings.ToLower(xhttp.ContentEncoding)) 156 } 157 } 158 159 // Success. 160 return metadata, nil 161 } 162 163 // extractMetadata extracts metadata from map values. 164 func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[string]string) error { 165 if v == nil { 166 logger.LogIf(ctx, errInvalidArgument) 167 return errInvalidArgument 168 } 169 170 nv := make(textproto.MIMEHeader, len(v)) 171 for k, kv := range v { 172 // Canonicalize all headers, to remove any duplicates. 173 nv[http.CanonicalHeaderKey(k)] = kv 174 } 175 176 // Save all supported headers. 177 for _, supportedHeader := range supportedHeaders { 178 value, ok := nv[http.CanonicalHeaderKey(supportedHeader)] 179 if ok { 180 m[supportedHeader] = strings.Join(value, ",") 181 } 182 } 183 184 for key := range v { 185 for _, prefix := range userMetadataKeyPrefixes { 186 if !strings.HasPrefix(strings.ToLower(key), strings.ToLower(prefix)) { 187 continue 188 } 189 value, ok := nv[http.CanonicalHeaderKey(key)] 190 if ok { 191 m[key] = strings.Join(value, ",") 192 break 193 } 194 } 195 } 196 return nil 197 } 198 199 // The Query string for the redirect URL the client is 200 // redirected on successful upload. 201 func getRedirectPostRawQuery(objInfo ObjectInfo) string { 202 redirectValues := make(url.Values) 203 redirectValues.Set("bucket", objInfo.Bucket) 204 redirectValues.Set("key", objInfo.Name) 205 redirectValues.Set("etag", "\""+objInfo.ETag+"\"") 206 return redirectValues.Encode() 207 } 208 209 // Returns access credentials in the request Authorization header. 210 func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) { 211 cred, _, _ = getReqAccessKeyV4(r, region, serviceS3) 212 if cred.AccessKey == "" { 213 cred, _, _ = getReqAccessKeyV2(r) 214 } 215 if cred.AccessKey == "" { 216 claims, owner, _ := webRequestAuthenticate(r) 217 if owner { 218 return globalActiveCred 219 } 220 if claims != nil { 221 cred, _ = GlobalIAMSys.GetUser(r.Context(), claims.AccessKey) 222 } 223 } 224 return cred 225 } 226 227 // Extract request params to be sent with event notifiation. 228 func extractReqParams(r *http.Request) map[string]string { 229 if r == nil { 230 return nil 231 } 232 233 region := globalServerRegion 234 cred := getReqAccessCred(r, region) 235 236 principalID := cred.AccessKey 237 if cred.ParentUser != "" { 238 principalID = cred.ParentUser 239 } 240 241 // Success. 242 m := map[string]string{ 243 "region": region, 244 "principalId": principalID, 245 "sourceIPAddress": handlers.GetSourceIP(r), 246 // Add more fields here. 247 } 248 if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok { 249 m[xhttp.MinIOSourceReplicationRequest] = "" 250 } 251 return m 252 } 253 254 // Extract response elements to be sent with event notifiation. 255 func extractRespElements(w http.ResponseWriter) map[string]string { 256 if w == nil { 257 return map[string]string{} 258 } 259 return map[string]string{ 260 "requestId": w.Header().Get(xhttp.AmzRequestID), 261 "content-length": w.Header().Get(xhttp.ContentLength), 262 // Add more fields here. 263 } 264 } 265 266 // Trims away `aws-chunked` from the content-encoding header if present. 267 // Streaming signature clients can have custom content-encoding such as 268 // `aws-chunked,gzip` here we need to only save `gzip`. 269 // For more refer http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html 270 func trimAwsChunkedContentEncoding(contentEnc string) (trimmedContentEnc string) { 271 if contentEnc == "" { 272 return contentEnc 273 } 274 var newEncs []string 275 for _, enc := range strings.Split(contentEnc, ",") { 276 if enc != streamingContentEncoding { 277 newEncs = append(newEncs, enc) 278 } 279 } 280 return strings.Join(newEncs, ",") 281 } 282 283 // Validate form field size for s3 specification requirement. 284 func validateFormFieldSize(ctx context.Context, formValues http.Header) error { 285 // Iterate over form values 286 for k := range formValues { 287 // Check if value's field exceeds S3 limit 288 if int64(len(formValues.Get(k))) > maxFormFieldSize { 289 logger.LogIf(ctx, errSizeUnexpected) 290 return errSizeUnexpected 291 } 292 } 293 294 // Success. 295 return nil 296 } 297 298 // Extract form fields and file data from a HTTP POST Policy 299 func extractPostPolicyFormValues(ctx context.Context, form *multipart.Form) (filePart io.ReadCloser, fileName string, fileSize int64, formValues http.Header, err error) { 300 /// HTML Form values 301 fileName = "" 302 303 // Canonicalize the form values into http.Header. 304 formValues = make(http.Header) 305 for k, v := range form.Value { 306 formValues[http.CanonicalHeaderKey(k)] = v 307 } 308 309 // Validate form values. 310 if err = validateFormFieldSize(ctx, formValues); err != nil { 311 return nil, "", 0, nil, err 312 } 313 314 // this means that filename="" was not specified for file key and Go has 315 // an ugly way of handling this situation. Refer here 316 // https://golang.org/src/mime/multipart/formdata.go#L61 317 if len(form.File) == 0 { 318 var b = &bytes.Buffer{} 319 for _, v := range formValues["File"] { 320 b.WriteString(v) 321 } 322 fileSize = int64(b.Len()) 323 filePart = ioutil.NopCloser(b) 324 return filePart, fileName, fileSize, formValues, nil 325 } 326 327 // Iterator until we find a valid File field and break 328 for k, v := range form.File { 329 canonicalFormName := http.CanonicalHeaderKey(k) 330 if canonicalFormName == "File" { 331 if len(v) == 0 { 332 logger.LogIf(ctx, errInvalidArgument) 333 return nil, "", 0, nil, errInvalidArgument 334 } 335 // Fetch fileHeader which has the uploaded file information 336 fileHeader := v[0] 337 // Set filename 338 fileName = fileHeader.Filename 339 // Open the uploaded part 340 filePart, err = fileHeader.Open() 341 if err != nil { 342 logger.LogIf(ctx, err) 343 return nil, "", 0, nil, err 344 } 345 // Compute file size 346 fileSize, err = filePart.(io.Seeker).Seek(0, 2) 347 if err != nil { 348 logger.LogIf(ctx, err) 349 return nil, "", 0, nil, err 350 } 351 // Reset Seek to the beginning 352 _, err = filePart.(io.Seeker).Seek(0, 0) 353 if err != nil { 354 logger.LogIf(ctx, err) 355 return nil, "", 0, nil, err 356 } 357 // File found and ready for reading 358 break 359 } 360 } 361 return filePart, fileName, fileSize, formValues, nil 362 } 363 364 // Log headers and body. 365 func HTTPTraceAll(f http.HandlerFunc) http.HandlerFunc { 366 return func(w http.ResponseWriter, r *http.Request) { 367 if globalTrace.NumSubscribers() == 0 { 368 f.ServeHTTP(w, r) 369 return 370 } 371 trace := Trace(f, true, w, r) 372 globalTrace.Publish(trace) 373 } 374 } 375 376 // Log only the headers. 377 func HTTPTraceHdrs(f http.HandlerFunc) http.HandlerFunc { 378 return func(w http.ResponseWriter, r *http.Request) { 379 if globalTrace.NumSubscribers() == 0 { 380 f.ServeHTTP(w, r) 381 return 382 } 383 trace := Trace(f, false, w, r) 384 globalTrace.Publish(trace) 385 } 386 } 387 388 func CollectAPIStats(api string, f http.HandlerFunc) http.HandlerFunc { 389 return func(w http.ResponseWriter, r *http.Request) { 390 globalHTTPStats.currentS3Requests.Inc(api) 391 defer globalHTTPStats.currentS3Requests.Dec(api) 392 393 statsWriter := logger.NewResponseWriter(w) 394 395 f.ServeHTTP(statsWriter, r) 396 397 globalHTTPStats.updateStats(api, r, statsWriter) 398 } 399 } 400 401 // Returns "/bucketName/objectName" for path-style or virtual-host-style requests. 402 func getResource(path string, host string, domains []string) (string, error) { 403 if len(domains) == 0 { 404 return path, nil 405 } 406 // If virtual-host-style is enabled construct the "resource" properly. 407 if strings.Contains(host, ":") { 408 // In bucket.mydomain.com:9000, strip out :9000 409 var err error 410 if host, _, err = net.SplitHostPort(host); err != nil { 411 reqInfo := (&logger.ReqInfo{}).AppendTags("host", host) 412 reqInfo.AppendTags("path", path) 413 ctx := logger.SetReqInfo(GlobalContext, reqInfo) 414 logger.LogIf(ctx, err) 415 return "", err 416 } 417 } 418 for _, domain := range domains { 419 if host == minioReservedBucket+"."+domain { 420 continue 421 } 422 if !strings.HasSuffix(host, "."+domain) { 423 continue 424 } 425 bucket := strings.TrimSuffix(host, "."+domain) 426 return SlashSeparator + pathJoin(bucket, path), nil 427 } 428 return path, nil 429 } 430 431 var regexVersion = regexp.MustCompile(`^/minio.*/(v\d+)/.*`) 432 433 func extractAPIVersion(r *http.Request) string { 434 if matches := regexVersion.FindStringSubmatch(r.URL.Path); len(matches) > 1 { 435 return matches[1] 436 } 437 return "unknown" 438 } 439 440 func MethodNotAllowedHandler(api string) func(w http.ResponseWriter, r *http.Request) { 441 return func(w http.ResponseWriter, r *http.Request) { 442 if r.Method == http.MethodOptions { 443 return 444 } 445 version := extractAPIVersion(r) 446 switch { 447 case strings.HasPrefix(r.URL.Path, peerRESTPrefix): 448 desc := fmt.Sprintf("Server expects 'peer' API version '%s', instead found '%s' - *rolling upgrade is not allowed* - please make sure all servers are running the same MinIO version (%s)", peerRESTVersion, version, ReleaseTag) 449 writeErrorResponseString(r.Context(), w, APIError{ 450 Code: "XMinioPeerVersionMismatch", 451 Description: desc, 452 HTTPStatusCode: http.StatusUpgradeRequired, 453 }, r.URL) 454 case strings.HasPrefix(r.URL.Path, storageRESTPrefix): 455 desc := fmt.Sprintf("Server expects 'storage' API version '%s', instead found '%s' - *rolling upgrade is not allowed* - please make sure all servers are running the same MinIO version (%s)", storageRESTVersion, version, ReleaseTag) 456 writeErrorResponseString(r.Context(), w, APIError{ 457 Code: "XMinioStorageVersionMismatch", 458 Description: desc, 459 HTTPStatusCode: http.StatusUpgradeRequired, 460 }, r.URL) 461 case strings.HasPrefix(r.URL.Path, lockRESTPrefix): 462 desc := fmt.Sprintf("Server expects 'lock' API version '%s', instead found '%s' - *rolling upgrade is not allowed* - please make sure all servers are running the same MinIO version (%s)", lockRESTVersion, version, ReleaseTag) 463 writeErrorResponseString(r.Context(), w, APIError{ 464 Code: "XMinioLockVersionMismatch", 465 Description: desc, 466 HTTPStatusCode: http.StatusUpgradeRequired, 467 }, r.URL) 468 case strings.HasPrefix(r.URL.Path, adminPathPrefix): 469 var desc string 470 if version == "v1" { 471 desc = fmt.Sprintf("Server expects client requests with 'admin' API version '%s', found '%s', please upgrade the client to latest releases", madmin.AdminAPIVersion, version) 472 } else if version == madmin.AdminAPIVersion { 473 desc = fmt.Sprintf("This 'admin' API is not supported by server in '%s'", getMinioMode()) 474 } else { 475 desc = fmt.Sprintf("Unexpected client 'admin' API version found '%s', expected '%s', please downgrade the client to older releases", version, madmin.AdminAPIVersion) 476 } 477 writeErrorResponseJSON(r.Context(), w, APIError{ 478 Code: "XMinioAdminVersionMismatch", 479 Description: desc, 480 HTTPStatusCode: http.StatusUpgradeRequired, 481 }, r.URL) 482 default: 483 WriteErrorResponse(r.Context(), w, APIError{ 484 Code: "BadRequest", 485 Description: fmt.Sprintf("An error occurred when parsing the HTTP request %s at '%s'", 486 r.Method, r.URL.Path), 487 HTTPStatusCode: http.StatusBadRequest, 488 }, r.URL, guessIsBrowserReq(r)) 489 } 490 } 491 } 492 493 // If none of the http routes match respond with appropriate errors 494 func ErrorResponseHandler(w http.ResponseWriter, r *http.Request) { 495 if r.Method == http.MethodOptions { 496 return 497 } 498 version := extractAPIVersion(r) 499 switch { 500 case strings.HasPrefix(r.URL.Path, peerRESTPrefix): 501 desc := fmt.Sprintf("Server expects 'peer' API version '%s', instead found '%s' - *rolling upgrade is not allowed* - please make sure all servers are running the same MinIO version (%s)", peerRESTVersion, version, ReleaseTag) 502 writeErrorResponseString(r.Context(), w, APIError{ 503 Code: "XMinioPeerVersionMismatch", 504 Description: desc, 505 HTTPStatusCode: http.StatusUpgradeRequired, 506 }, r.URL) 507 case strings.HasPrefix(r.URL.Path, storageRESTPrefix): 508 desc := fmt.Sprintf("Server expects 'storage' API version '%s', instead found '%s' - *rolling upgrade is not allowed* - please make sure all servers are running the same MinIO version (%s)", storageRESTVersion, version, ReleaseTag) 509 writeErrorResponseString(r.Context(), w, APIError{ 510 Code: "XMinioStorageVersionMismatch", 511 Description: desc, 512 HTTPStatusCode: http.StatusUpgradeRequired, 513 }, r.URL) 514 case strings.HasPrefix(r.URL.Path, lockRESTPrefix): 515 desc := fmt.Sprintf("Server expects 'lock' API version '%s', instead found '%s' - *rolling upgrade is not allowed* - please make sure all servers are running the same MinIO version (%s)", lockRESTVersion, version, ReleaseTag) 516 writeErrorResponseString(r.Context(), w, APIError{ 517 Code: "XMinioLockVersionMismatch", 518 Description: desc, 519 HTTPStatusCode: http.StatusUpgradeRequired, 520 }, r.URL) 521 case strings.HasPrefix(r.URL.Path, adminPathPrefix): 522 var desc string 523 if version == "v1" { 524 desc = fmt.Sprintf("Server expects client requests with 'admin' API version '%s', found '%s', please upgrade the client to latest releases", madmin.AdminAPIVersion, version) 525 } else if version == madmin.AdminAPIVersion { 526 desc = fmt.Sprintf("This 'admin' API is not supported by server in '%s'", getMinioMode()) 527 } else { 528 desc = fmt.Sprintf("Unexpected client 'admin' API version found '%s', expected '%s', please downgrade the client to older releases", version, madmin.AdminAPIVersion) 529 } 530 writeErrorResponseJSON(r.Context(), w, APIError{ 531 Code: "XMinioAdminVersionMismatch", 532 Description: desc, 533 HTTPStatusCode: http.StatusUpgradeRequired, 534 }, r.URL) 535 default: 536 WriteErrorResponse(r.Context(), w, APIError{ 537 Code: "BadRequest", 538 Description: fmt.Sprintf("An error occurred when parsing the HTTP request %s at '%s'", 539 r.Method, r.URL.Path), 540 HTTPStatusCode: http.StatusBadRequest, 541 }, r.URL, guessIsBrowserReq(r)) 542 } 543 544 } 545 546 // gets host name for current node 547 func getHostName(r *http.Request) (hostName string) { 548 if globalIsDistErasure { 549 hostName = globalLocalNodeName 550 } else { 551 hostName = r.Host 552 } 553 return 554 } 555 556 // Proxy any request to an endpoint. 557 func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ep ProxyEndpoint) (success bool) { 558 success = true 559 560 // Make sure we remove any existing headers before 561 // proxying the request to another node. 562 for k := range w.Header() { 563 w.Header().Del(k) 564 } 565 566 f := handlers.NewForwarder(&handlers.Forwarder{ 567 PassHost: true, 568 RoundTripper: ep.Transport, 569 ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { 570 success = false 571 if err != nil && !errors.Is(err, context.Canceled) { 572 logger.LogIf(GlobalContext, err) 573 } 574 }, 575 }) 576 577 r.URL.Scheme = "http" 578 if GlobalIsTLS { 579 r.URL.Scheme = "https" 580 } 581 582 r.URL.Host = ep.Host 583 f.ServeHTTP(w, r) 584 return 585 }