storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/generic-handlers.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 "context" 21 "net/http" 22 "strings" 23 "sync/atomic" 24 "time" 25 26 humanize "github.com/dustin/go-humanize" 27 28 "storj.io/minio/cmd/crypto" 29 xhttp "storj.io/minio/cmd/http" 30 "storj.io/minio/cmd/http/stats" 31 "storj.io/minio/cmd/logger" 32 ) 33 34 // Adds limiting body size middleware 35 36 // Maximum allowed form data field values. 64MiB is a guessed practical value 37 // which is more than enough to accommodate any form data fields and headers. 38 const requestFormDataSize = 64 * humanize.MiByte 39 40 // For any HTTP request, request body should be not more than 16GiB + requestFormDataSize 41 // where, 16GiB is the maximum allowed object size for object upload. 42 const requestMaxBodySize = globalMaxObjectSize + requestFormDataSize 43 44 func setRequestSizeLimitHandler(h http.Handler) http.Handler { 45 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 // Restricting read data to a given maximum length 47 r.Body = http.MaxBytesReader(w, r.Body, requestMaxBodySize) 48 h.ServeHTTP(w, r) 49 }) 50 } 51 52 const ( 53 // Maximum size for http headers - See: https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html 54 maxHeaderSize = 8 * 1024 55 // Maximum size for user-defined metadata - See: https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html 56 maxUserDataSize = 2 * 1024 57 ) 58 59 // ServeHTTP restricts the size of the http header to 8 KB and the size 60 // of the user-defined metadata to 2 KB. 61 func setRequestHeaderSizeLimitHandler(h http.Handler) http.Handler { 62 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 63 if isHTTPHeaderSizeTooLarge(r.Header) { 64 WriteErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrMetadataTooLarge), r.URL, guessIsBrowserReq(r)) 65 atomic.AddUint64(&globalHTTPStats.rejectedRequestsHeader, 1) 66 return 67 } 68 h.ServeHTTP(w, r) 69 }) 70 } 71 72 // isHTTPHeaderSizeTooLarge returns true if the provided 73 // header is larger than 8 KB or the user-defined metadata 74 // is larger than 2 KB. 75 func isHTTPHeaderSizeTooLarge(header http.Header) bool { 76 var size, usersize int 77 for key := range header { 78 length := len(key) + len(header.Get(key)) 79 size += length 80 for _, prefix := range userMetadataKeyPrefixes { 81 if strings.HasPrefix(strings.ToLower(key), prefix) { 82 usersize += length 83 break 84 } 85 } 86 if usersize > maxUserDataSize || size > maxHeaderSize { 87 return true 88 } 89 } 90 return false 91 } 92 93 // ReservedMetadataPrefix is the prefix of a metadata key which 94 // is reserved and for internal use only. 95 const ( 96 ReservedMetadataPrefix = "X-Minio-Internal-" 97 ReservedMetadataPrefixLower = "x-minio-internal-" 98 ) 99 100 // ServeHTTP fails if the request contains at least one reserved header which 101 // would be treated as metadata. 102 func filterReservedMetadata(h http.Handler) http.Handler { 103 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 104 if containsReservedMetadata(r.Header) { 105 WriteErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrUnsupportedMetadata), r.URL, guessIsBrowserReq(r)) 106 return 107 } 108 h.ServeHTTP(w, r) 109 }) 110 } 111 112 // containsReservedMetadata returns true if the http.Header contains 113 // keys which are treated as metadata but are reserved for internal use 114 // and must not set by clients 115 func containsReservedMetadata(header http.Header) bool { 116 for key := range header { 117 if strings.HasPrefix(strings.ToLower(key), ReservedMetadataPrefixLower) { 118 return true 119 } 120 } 121 return false 122 } 123 124 // Reserved bucket. 125 const ( 126 minioReservedBucket = "minio" 127 minioReservedBucketPath = SlashSeparator + minioReservedBucket 128 loginPathPrefix = SlashSeparator + "login" 129 ) 130 131 func setRedirectHandler(h http.Handler) http.Handler { 132 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 133 if !shouldProxy() || guessIsRPCReq(r) || guessIsBrowserReq(r) || 134 guessIsHealthCheckReq(r) || guessIsMetricsReq(r) || isAdminReq(r) { 135 h.ServeHTTP(w, r) 136 return 137 } 138 // if this server is still initializing, proxy the request 139 // to any other online servers to avoid 503 for any incoming 140 // API calls. 141 if idx := getOnlineProxyEndpointIdx(); idx >= 0 { 142 proxyRequest(context.TODO(), w, r, globalProxyEndpoints[idx]) 143 return 144 } 145 h.ServeHTTP(w, r) 146 }) 147 } 148 149 func setBrowserRedirectHandler(h http.Handler) http.Handler { 150 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 151 // Re-direction is handled specifically for browser requests. 152 if globalBrowserEnabled && guessIsBrowserReq(r) { 153 // Fetch the redirect location if any. 154 redirectLocation := getRedirectLocation(r.URL.Path) 155 if redirectLocation != "" { 156 // Employ a temporary re-direct. 157 http.Redirect(w, r, redirectLocation, http.StatusTemporaryRedirect) 158 return 159 } 160 } 161 h.ServeHTTP(w, r) 162 }) 163 } 164 165 func shouldProxy() bool { 166 if newObjectLayerFn() == nil { 167 return true 168 } 169 return !GlobalIAMSys.Initialized() 170 } 171 172 // Fetch redirect location if urlPath satisfies certain 173 // criteria. Some special names are considered to be 174 // redirectable, this is purely internal function and 175 // serves only limited purpose on redirect-handler for 176 // browser requests. 177 func getRedirectLocation(urlPath string) (rLocation string) { 178 if urlPath == minioReservedBucketPath { 179 rLocation = minioReservedBucketPath + SlashSeparator 180 } 181 if contains([]string{ 182 SlashSeparator, 183 "/webrpc", 184 "/login", 185 "/favicon-16x16.png", 186 "/favicon-32x32.png", 187 "/favicon-96x96.png", 188 }, urlPath) { 189 rLocation = minioReservedBucketPath + urlPath 190 } 191 return rLocation 192 } 193 194 // guessIsBrowserReq - returns true if the request is browser. 195 // This implementation just validates user-agent and 196 // looks for "Mozilla" string. This is no way certifiable 197 // way to know if the request really came from a browser 198 // since User-Agent's can be arbitrary. But this is just 199 // a best effort function. 200 func guessIsBrowserReq(req *http.Request) bool { 201 if req == nil { 202 return false 203 } 204 aType := getRequestAuthType(req) 205 return strings.Contains(req.Header.Get("User-Agent"), "Mozilla") && globalBrowserEnabled && 206 (aType == authTypeJWT || aType == authTypeAnonymous) 207 } 208 209 // guessIsHealthCheckReq - returns true if incoming request looks 210 // like healthcheck request 211 func guessIsHealthCheckReq(req *http.Request) bool { 212 if req == nil { 213 return false 214 } 215 aType := getRequestAuthType(req) 216 return aType == authTypeAnonymous && (req.Method == http.MethodGet || req.Method == http.MethodHead) && 217 (req.URL.Path == healthCheckPathPrefix+healthCheckLivenessPath || 218 req.URL.Path == healthCheckPathPrefix+healthCheckReadinessPath || 219 req.URL.Path == healthCheckPathPrefix+healthCheckClusterPath || 220 req.URL.Path == healthCheckPathPrefix+healthCheckClusterReadPath) 221 } 222 223 // guessIsMetricsReq - returns true if incoming request looks 224 // like metrics request 225 func guessIsMetricsReq(req *http.Request) bool { 226 if req == nil { 227 return false 228 } 229 aType := getRequestAuthType(req) 230 return (aType == authTypeAnonymous || aType == authTypeJWT) && 231 req.URL.Path == minioReservedBucketPath+prometheusMetricsPathLegacy || 232 req.URL.Path == minioReservedBucketPath+prometheusMetricsV2ClusterPath || 233 req.URL.Path == minioReservedBucketPath+prometheusMetricsV2NodePath 234 } 235 236 // guessIsRPCReq - returns true if the request is for an RPC endpoint. 237 func guessIsRPCReq(req *http.Request) bool { 238 if req == nil { 239 return false 240 } 241 return req.Method == http.MethodPost && 242 strings.HasPrefix(req.URL.Path, minioReservedBucketPath+SlashSeparator) 243 } 244 245 // Adds Cache-Control header 246 func setBrowserCacheControlHandler(h http.Handler) http.Handler { 247 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 248 if globalBrowserEnabled && r.Method == http.MethodGet && guessIsBrowserReq(r) { 249 // For all browser requests set appropriate Cache-Control policies 250 if HasPrefix(r.URL.Path, minioReservedBucketPath+SlashSeparator) { 251 if HasSuffix(r.URL.Path, ".js") || r.URL.Path == minioReservedBucketPath+"/favicon.ico" { 252 // For assets set cache expiry of one year. For each release, the name 253 // of the asset name will change and hence it can not be served from cache. 254 w.Header().Set(xhttp.CacheControl, "max-age=31536000") 255 } else { 256 // For non asset requests we serve index.html which will never be cached. 257 w.Header().Set(xhttp.CacheControl, "no-store") 258 } 259 } 260 } 261 262 h.ServeHTTP(w, r) 263 }) 264 } 265 266 // Check to allow access to the reserved "bucket" `/minio` for Admin 267 // API requests. 268 func isAdminReq(r *http.Request) bool { 269 return strings.HasPrefix(r.URL.Path, adminPathPrefix) 270 } 271 272 // guessIsLoginSTSReq - returns true if incoming request is Login STS user 273 func guessIsLoginSTSReq(req *http.Request) bool { 274 if req == nil { 275 return false 276 } 277 return strings.HasPrefix(req.URL.Path, loginPathPrefix) || 278 (req.Method == http.MethodPost && req.URL.Path == SlashSeparator && 279 getRequestAuthType(req) == authTypeSTS) 280 } 281 282 // Adds verification for incoming paths. 283 func setReservedBucketHandler(h http.Handler) http.Handler { 284 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 285 // For all other requests reject access to reserved buckets 286 bucketName, _ := request2BucketObjectName(r) 287 if isMinioReservedBucket(bucketName) || isMinioMetaBucket(bucketName) { 288 if !guessIsRPCReq(r) && !guessIsBrowserReq(r) && !guessIsHealthCheckReq(r) && !guessIsMetricsReq(r) && !isAdminReq(r) { 289 WriteErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrAllAccessDisabled), r.URL, guessIsBrowserReq(r)) 290 return 291 } 292 } 293 h.ServeHTTP(w, r) 294 }) 295 } 296 297 // Supported Amz date formats. 298 var amzDateFormats = []string{ 299 time.RFC1123, 300 time.RFC1123Z, 301 iso8601Format, 302 // Add new AMZ date formats here. 303 } 304 305 // Supported Amz date headers. 306 var amzDateHeaders = []string{ 307 "x-amz-date", 308 "date", 309 } 310 311 // parseAmzDate - parses date string into supported amz date formats. 312 func parseAmzDate(amzDateStr string) (amzDate time.Time, apiErr APIErrorCode) { 313 for _, dateFormat := range amzDateFormats { 314 amzDate, err := time.Parse(dateFormat, amzDateStr) 315 if err == nil { 316 return amzDate, ErrNone 317 } 318 } 319 return time.Time{}, ErrMissingDateHeader 320 } 321 322 // parseAmzDateHeader - parses supported amz date headers, in 323 // supported amz date formats. 324 func parseAmzDateHeader(req *http.Request) (time.Time, APIErrorCode) { 325 for _, amzDateHeader := range amzDateHeaders { 326 amzDateStr := req.Header.Get(amzDateHeader) 327 if amzDateStr != "" { 328 return parseAmzDate(amzDateStr) 329 } 330 } 331 // Date header missing. 332 return time.Time{}, ErrMissingDateHeader 333 } 334 335 // setTimeValidityHandler to validate parsable time over http header 336 func setTimeValidityHandler(h http.Handler) http.Handler { 337 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 338 aType := getRequestAuthType(r) 339 if aType == authTypeSigned || aType == authTypeSignedV2 || aType == authTypeStreamingSigned { 340 // Verify if date headers are set, if not reject the request 341 amzDate, errCode := parseAmzDateHeader(r) 342 if errCode != ErrNone { 343 // All our internal APIs are sensitive towards Date 344 // header, for all requests where Date header is not 345 // present we will reject such clients. 346 WriteErrorResponse(r.Context(), w, errorCodes.ToAPIErr(errCode), r.URL, guessIsBrowserReq(r)) 347 atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1) 348 return 349 } 350 // Verify if the request date header is shifted by less than globalMaxSkewTime parameter in the past 351 // or in the future, reject request otherwise. 352 curTime := UTCNow() 353 if curTime.Sub(amzDate) > globalMaxSkewTime || amzDate.Sub(curTime) > globalMaxSkewTime { 354 WriteErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrRequestTimeTooSkewed), r.URL, guessIsBrowserReq(r)) 355 atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1) 356 return 357 } 358 } 359 h.ServeHTTP(w, r) 360 }) 361 } 362 363 // setHttpStatsHandler sets a http Stats handler to gather HTTP statistics 364 func setHTTPStatsHandler(h http.Handler) http.Handler { 365 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 366 // Meters s3 connection stats. 367 meteredRequest := &stats.IncomingTrafficMeter{ReadCloser: r.Body} 368 meteredResponse := &stats.OutgoingTrafficMeter{ResponseWriter: w} 369 370 // Execute the request 371 r.Body = meteredRequest 372 h.ServeHTTP(meteredResponse, r) 373 374 if strings.HasPrefix(r.URL.Path, minioReservedBucketPath) { 375 globalConnStats.incInputBytes(meteredRequest.BytesCount()) 376 globalConnStats.incOutputBytes(meteredResponse.BytesCount()) 377 } else { 378 globalConnStats.incS3InputBytes(meteredRequest.BytesCount()) 379 globalConnStats.incS3OutputBytes(meteredResponse.BytesCount()) 380 } 381 }) 382 } 383 384 // Bad path components to be rejected by the path validity handler. 385 const ( 386 dotdotComponent = ".." 387 dotComponent = "." 388 ) 389 390 // Check if the incoming path has bad path components, 391 // such as ".." and "." 392 func hasBadPathComponent(path string) bool { 393 path = strings.TrimSpace(path) 394 for _, p := range strings.Split(path, SlashSeparator) { 395 switch strings.TrimSpace(p) { 396 case dotdotComponent: 397 return true 398 case dotComponent: 399 return true 400 } 401 } 402 return false 403 } 404 405 // Check if client is sending a malicious request. 406 func hasMultipleAuth(r *http.Request) bool { 407 authTypeCount := 0 408 for _, hasValidAuth := range []func(*http.Request) bool{isRequestSignatureV2, isRequestPresignedSignatureV2, isRequestSignatureV4, isRequestPresignedSignatureV4, isRequestJWT, isRequestPostPolicySignatureV4} { 409 if hasValidAuth(r) { 410 authTypeCount++ 411 } 412 } 413 return authTypeCount > 1 414 } 415 416 // requestValidityHandler validates all the incoming paths for 417 // any malicious requests. 418 func setRequestValidityHandler(h http.Handler) http.Handler { 419 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 420 // Check for bad components in URL path. 421 if hasBadPathComponent(r.URL.Path) { 422 WriteErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL, guessIsBrowserReq(r)) 423 atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1) 424 return 425 } 426 // Check for bad components in URL query values. 427 for q, vv := range r.URL.Query() { 428 // TODO: ideally we'd have a precise list of fields to check here 429 // however, we know that "delimiter" must be skipped for the Ceph / Splunk tests 430 if q == "delimiter" { 431 continue 432 } 433 for _, v := range vv { 434 if hasBadPathComponent(v) { 435 WriteErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL, guessIsBrowserReq(r)) 436 atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1) 437 return 438 } 439 } 440 } 441 if hasMultipleAuth(r) { 442 WriteErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r)) 443 atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1) 444 return 445 } 446 h.ServeHTTP(w, r) 447 }) 448 } 449 450 // customHeaderHandler sets x-amz-request-id header. 451 // Previously, this value was set right before a response was sent to 452 // the client. So, logger and Error response XML were not using this 453 // value. This is set here so that this header can be logged as 454 // part of the log entry, Error response XML and auditing. 455 func addCustomHeaders(h http.Handler) http.Handler { 456 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 457 // Set custom headers such as x-amz-request-id for each request. 458 w.Header().Set(xhttp.AmzRequestID, mustGetRequestID(UTCNow())) 459 h.ServeHTTP(logger.NewResponseWriter(w), r) 460 }) 461 } 462 463 func addSecurityHeaders(h http.Handler) http.Handler { 464 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 465 header := w.Header() 466 header.Set("X-XSS-Protection", "1; mode=block") // Prevents against XSS attacks 467 header.Set("Content-Security-Policy", "block-all-mixed-content") // prevent mixed (HTTP / HTTPS content) 468 h.ServeHTTP(w, r) 469 }) 470 } 471 472 // criticalErrorHandler handles critical server failures caused by 473 // `panic(logger.ErrCritical)` as done by `logger.CriticalIf`. 474 // 475 // It should be always the first / highest HTTP handler. 476 type criticalErrorHandler struct{ handler http.Handler } 477 478 func (h criticalErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 479 defer func() { 480 if err := recover(); err == logger.ErrCritical { // handle 481 WriteErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInternalError), r.URL, guessIsBrowserReq(r)) 482 return 483 } else if err != nil { 484 panic(err) // forward other panic calls 485 } 486 }() 487 h.handler.ServeHTTP(w, r) 488 } 489 490 // sseTLSHandler enforces certain rules for SSE requests which are made / must be made over TLS. 491 func setSSETLSHandler(h http.Handler) http.Handler { 492 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 493 // Deny SSE-C requests if not made over TLS 494 if !GlobalIsTLS && (crypto.SSEC.IsRequested(r.Header) || crypto.SSECopy.IsRequested(r.Header)) { 495 if r.Method == http.MethodHead { 496 writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrInsecureSSECustomerRequest)) 497 } else { 498 WriteErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInsecureSSECustomerRequest), r.URL, guessIsBrowserReq(r)) 499 } 500 return 501 } 502 h.ServeHTTP(w, r) 503 }) 504 }