public void RemoveHeader_RemovesProxyRequestHeader(string names, string removedHeader, string expected) { var httpContext = new DefaultHttpContext(); var proxyRequest = new HttpRequestMessage() { Content = new EmptyHttpContent() }; foreach (var name in names.Split("; ")) { httpContext.Request.Headers.Add(name, "value0"); RequestUtilities.AddHeader(proxyRequest, name, "value1"); } RequestTransform.RemoveHeader(new RequestTransformContext() { HttpContext = httpContext, ProxyRequest = proxyRequest }, removedHeader); foreach (var name in names.Split("; ")) { Assert.True(httpContext.Request.Headers.TryGetValue(name, out var value)); Assert.Equal("value0", value); } var expectedHeaders = expected.Split("; ", System.StringSplitOptions.RemoveEmptyEntries).OrderBy(h => h); var remainingHeaders = proxyRequest.Headers.Union(proxyRequest.Content.Headers).OrderBy(h => h.Key); Assert.Equal(expectedHeaders, remainingHeaders.Select(h => h.Key)); Assert.All(remainingHeaders, h => Assert.Equal("value1", Assert.Single(h.Value))); }
private void CopyRequestHeaders(HttpContext context, HttpRequestMessage proxyRequest) { // Transforms that were run in the first pass. HashSet <string> transformsRun = null; if (ShouldCopyRequestHeaders.GetValueOrDefault(true)) { foreach (var header in context.Request.Headers) { var headerName = header.Key; var headerValue = header.Value; if (StringValues.IsNullOrEmpty(headerValue)) { continue; } // Filter out HTTP/2 pseudo headers like ":method" and ":path", those go into other fields. if (headerName.Length > 0 && headerName[0] == ':') { continue; } if (RequestHeaderTransforms.TryGetValue(headerName, out var transform)) { (transformsRun ??= new HashSet <string>(StringComparer.OrdinalIgnoreCase)).Add(headerName); headerValue = transform.Apply(context, proxyRequest, headerValue); if (StringValues.IsNullOrEmpty(headerValue)) { continue; } } RequestUtilities.AddHeader(proxyRequest, headerName, headerValue); } } transformsRun ??= EmptyHash; // Run any transforms that weren't run yet. foreach (var(headerName, transform) in RequestHeaderTransforms) { if (!transformsRun.Contains(headerName)) { var headerValue = transform.Apply(context, proxyRequest, StringValues.Empty); if (!StringValues.IsNullOrEmpty(headerValue)) { RequestUtilities.AddHeader(proxyRequest, headerName, headerValue); } } } }
/// <summary> /// Adds the given header to the HttpRequestMessage or HttpContent where applicable. /// </summary> public static void AddHeader(RequestTransformContext context, string headerName, StringValues values) { if (context is null) { throw new System.ArgumentNullException(nameof(context)); } if (string.IsNullOrEmpty(headerName)) { throw new System.ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName)); } RequestUtilities.AddHeader(context.ProxyRequest, headerName, values); }
/// <summary> /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage fields are /// initialized except RequestUri, which will be initialized after the callback if no value is provided. /// See <see cref="RequestUtilities.MakeDestinationAddress(string, PathString, QueryString)"/> for constructing a custom request Uri. /// The string parameter represents the destination URI prefix that should be used when constructing the RequestUri. /// The headers are copied by the base implementation, excluding some protocol headers like HTTP/2 pseudo headers (":authority"). /// </summary> /// <param name="httpContext">The incoming request.</param> /// <param name="proxyRequest">The outgoing proxy request.</param> /// <param name="destinationPrefix">The uri prefix for the selected destination server which can be used to create the RequestUri.</param> public virtual ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix) { foreach (var header in httpContext.Request.Headers) { var headerName = header.Key; var headerValue = header.Value; if (RequestUtilities.ShouldSkipRequestHeader(headerName)) { continue; } RequestUtilities.AddHeader(proxyRequest, headerName, headerValue); } // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 // If a message is received with both a Transfer-Encoding and a // Content-Length header field, the Transfer-Encoding overrides the // Content-Length. Such a message might indicate an attempt to // perform request smuggling (Section 9.5) or response splitting // (Section 9.4) and ought to be handled as an error. A sender MUST // remove the received Content-Length field prior to forwarding such // a message downstream. if (httpContext.Request.Headers.ContainsKey(HeaderNames.TransferEncoding) && httpContext.Request.Headers.ContainsKey(HeaderNames.ContentLength)) { proxyRequest.Content?.Headers.Remove(HeaderNames.ContentLength); } // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 // The only exception to this is the TE header field, which MAY be // present in an HTTP/2 request; when it is, it MUST NOT contain any // value other than "trailers". if (ProtocolHelper.IsHttp2OrGreater(httpContext.Request.Protocol)) { var te = httpContext.Request.Headers.GetCommaSeparatedValues(HeaderNames.TE); if (te != null) { for (var i = 0; i < te.Length; i++) { if (string.Equals(te[i], "trailers", StringComparison.OrdinalIgnoreCase)) { var added = proxyRequest.Headers.TryAddWithoutValidation(HeaderNames.TE, te[i]); Debug.Assert(added); break; } } } } return(default);
/// <summary> /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage fields are /// initialized except RequestUri, which will be initialized after the callback if no value is provided. /// See <see cref="RequestUtilities.MakeDestinationAddress(string, PathString, QueryString)"/> for constructing a custom request Uri. /// The string parameter represents the destination URI prefix that should be used when constructing the RequestUri. /// The headers are copied by the base implementation, excluding some protocol headers like HTTP/2 pseudo headers (":authority"). /// </summary> /// <param name="httpContext">The incoming request.</param> /// <param name="proxyRequest">The outgoing proxy request.</param> /// <param name="destinationPrefix">The uri prefix for the selected destination server which can be used to create the RequestUri.</param> public virtual ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix) { foreach (var header in httpContext.Request.Headers) { var headerName = header.Key; var headerValue = header.Value; if (StringValues.IsNullOrEmpty(headerValue)) { continue; } // Filter out HTTP/2 pseudo headers like ":method" and ":path", those go into other fields. if (headerName.Length > 0 && headerName[0] == ':') { continue; } RequestUtilities.AddHeader(proxyRequest, headerName, headerValue); } return(default);
/// <summary> /// Adds the given header to the HttpRequestMessage or HttpContent where applicable. /// </summary> public static void AddHeader(RequestTransformContext context, string headerName, StringValues values) { RequestUtilities.AddHeader(context.ProxyRequest, headerName, values); }