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 }