public async Task ProxyAsync_NormalRequestWithExistingForwarders_Appends() { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers.Add(":authority", "example.com:3456"); httpContext.Request.Headers.Add("x-forwarded-for", "::1"); httpContext.Request.Headers.Add("x-forwarded-proto", "https"); httpContext.Request.Headers.Add("x-forwarded-host", "some.other.host:4567"); httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var targetUri = new Uri("https://localhost:123/a/b/api/test"); var sut = Create <HttpProxy>(); var client = MockHttpHandler.CreateClient( async(HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(HttpMethod.Get, request.Method); Assert.Equal(targetUri, request.RequestUri); Assert.Equal(new[] { "::1", "127.0.0.1" }, request.Headers.GetValues("x-forwarded-for")); Assert.Equal(new[] { "https", "http" }, request.Headers.GetValues("x-forwarded-proto")); Assert.Equal(new[] { "some.other.host:4567", "example.com:3456" }, request.Headers.GetValues("x-forwarded-host")); Assert.Null(request.Headers.Host); Assert.False(request.Headers.TryGetValues(":authority", out var value)); // The proxy throws if the request body is not read. await request.Content.CopyToAsync(Stream.Null); var response = new HttpResponseMessage((HttpStatusCode)234); return(response); }); var factoryMock = new Mock <IProxyHttpClientFactory>(); factoryMock.Setup(f => f.CreateNormalClient()).Returns(client); var proxyTelemetryContext = new ProxyTelemetryContext( backendId: "be1", routeId: "rt1", destinationId: "d1"); // Act await sut.ProxyAsync(httpContext, targetUri, factoryMock.Object, proxyTelemetryContext, CancellationToken.None, CancellationToken.None); // Assert Assert.Equal(234, httpContext.Response.StatusCode); }
public async Task ProxyAsync_RequetsWithBodies_HasHttpContent(string method, string protocol, string headers) { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = method; httpContext.Request.Protocol = protocol; foreach (var header in headers.Split(';', StringSplitOptions.RemoveEmptyEntries)) { var parts = header.Split(':'); var key = parts[0]; var value = parts[1]; httpContext.Request.Headers[key] = value; } var destinationPrefix = "https://localhost/"; var sut = Create <HttpProxy>(); var client = MockHttpHandler.CreateClient( async(HttpRequestMessage request, CancellationToken cancellationToken) => { Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(method, request.Method.Method, StringComparer.OrdinalIgnoreCase); Assert.NotNull(request.Content); // Must consume the body await request.Content.CopyToAsync(Stream.Null); return(new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Array.Empty <byte>()) }); }); var factoryMock = new Mock <IProxyHttpClientFactory>(); factoryMock.Setup(f => f.CreateClient()).Returns(client); var proxyTelemetryContext = new ProxyTelemetryContext( clusterId: "be1", routeId: "rt1", destinationId: "d1"); await sut.ProxyAsync(httpContext, destinationPrefix, Transforms.Empty, factoryMock.Object, proxyTelemetryContext, CancellationToken.None, CancellationToken.None); Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); }
public async Task ProxyAsync_NormalRequest_Works() { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Scheme = "https"; httpContext.Request.Host = new HostString("example.com"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers.Add(":host", "example.com"); httpContext.Request.Headers.Add("x-ms-request-test", "request"); httpContext.Request.Headers.Add("Content-Language", "requestLanguage"); httpContext.Request.Body = StringToStream("request content"); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var targetUri = new Uri("https://localhost:123/a/b/api/test"); var sut = Create <HttpProxy>(); var client = MockHttpHandler.CreateClient( async(HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); request.Version.Should().BeEquivalentTo(new Version(2, 0)); request.Method.Should().Be(HttpMethod.Post); request.RequestUri.Should().Be(targetUri); request.Headers.GetValues("x-ms-request-test").Should().BeEquivalentTo("request"); request.Content.Should().NotBeNull(); request.Content.Headers.GetValues("Content-Language").Should().BeEquivalentTo("requestLanguage"); var capturedRequestContent = new MemoryStream(); // Use CopyToAsync as this is what HttpClient and friends use internally await request.Content.CopyToAsync(capturedRequestContent); capturedRequestContent.Position = 0; var capturedContentText = StreamToString(capturedRequestContent); capturedContentText.Should().Be("request content"); var response = new HttpResponseMessage((HttpStatusCode)234); response.ReasonPhrase = "Test Reason Phrase"; response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); response.Content = new StreamContent(StringToStream("response content")); response.Content.Headers.TryAddWithoutValidation("Content-Language", "responseLanguage"); return(response); }); var factoryMock = new Mock <IProxyHttpClientFactory>(); factoryMock.Setup(f => f.CreateNormalClient()).Returns(client); var proxyTelemetryContext = new ProxyTelemetryContext( backendId: "be1", routeId: "rt1", endpointId: "ep1"); // Act await sut.ProxyAsync(httpContext, targetUri, factoryMock.Object, proxyTelemetryContext, CancellationToken.None, CancellationToken.None); // Assert httpContext.Response.StatusCode.Should().Be(234); var reasonPhrase = httpContext.Features.Get <IHttpResponseFeature>().ReasonPhrase; reasonPhrase.Should().Be("Test Reason Phrase"); httpContext.Response.Headers["x-ms-response-test"].Should().BeEquivalentTo("response"); httpContext.Response.Headers["Content-Language"].Should().BeEquivalentTo("responseLanguage"); proxyResponseStream.Position = 0; var proxyResponseText = StreamToString(proxyResponseStream); proxyResponseText.Should().Be("response content"); }
public async Task ProxyAsync_UpgradableRequestFailsToUpgrade_ProxiesResponse() { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "https"; httpContext.Request.Host = new HostString("example.com"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers.Add(":host", "example.com"); httpContext.Request.Headers.Add("x-ms-request-test", "request"); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var upgradeFeatureMock = new Mock <IHttpUpgradeFeature>(MockBehavior.Strict); upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true); httpContext.Features.Set(upgradeFeatureMock.Object); var targetUri = new Uri("https://localhost:123/a/b/api/test?a=b&c=d"); var sut = Create <HttpProxy>(); var client = MockHttpHandler.CreateClient( async(HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); request.Version.Should().BeEquivalentTo(new Version(1, 1)); request.Method.Should().Be(HttpMethod.Get); request.RequestUri.Should().Be(targetUri); request.Headers.GetValues("x-ms-request-test").Should().BeEquivalentTo("request"); request.Content.Should().BeNull(); var response = new HttpResponseMessage((HttpStatusCode)234); response.ReasonPhrase = "Test Reason Phrase"; response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); response.Content = new StreamContent(StringToStream("response content")); response.Content.Headers.TryAddWithoutValidation("Content-Language", "responseLanguage"); return(response); }); var factoryMock = new Mock <IProxyHttpClientFactory>(); factoryMock.Setup(f => f.CreateUpgradableClient()).Returns(client); var proxyTelemetryContext = new ProxyTelemetryContext( backendId: "be1", routeId: "rt1", endpointId: "ep1"); // Act await sut.ProxyAsync(httpContext, targetUri, factoryMock.Object, proxyTelemetryContext, CancellationToken.None, CancellationToken.None); // Assert httpContext.Response.StatusCode.Should().Be(234); var reasonPhrase = httpContext.Features.Get <IHttpResponseFeature>().ReasonPhrase; reasonPhrase.Should().Be("Test Reason Phrase"); httpContext.Response.Headers["x-ms-response-test"].Should().BeEquivalentTo("response"); httpContext.Response.Headers["Content-Language"].Should().BeEquivalentTo("responseLanguage"); proxyResponseStream.Position = 0; var proxyResponseText = StreamToString(proxyResponseStream); proxyResponseText.Should().Be("response content"); }
public async Task ProxyAsync_UpgradableRequest_Works() { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "https"; httpContext.Request.Host = new HostString("example.com"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers.Add(":host", "example.com"); httpContext.Request.Headers.Add("x-ms-request-test", "request"); var downstreamStream = new DuplexStream( readStream: StringToStream("request content"), writeStream: new MemoryStream()); DuplexStream upstreamStream = null; var upgradeFeatureMock = new Mock <IHttpUpgradeFeature>(); upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true); upgradeFeatureMock.Setup(u => u.UpgradeAsync()).ReturnsAsync(downstreamStream); httpContext.Features.Set(upgradeFeatureMock.Object); var targetUri = new Uri("https://localhost:123/a/b/api/test?a=b&c=d"); var sut = Create <HttpProxy>(); var client = MockHttpHandler.CreateClient( async(HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); request.Version.Should().BeEquivalentTo(new Version(1, 1)); request.Method.Should().Be(HttpMethod.Get); request.RequestUri.Should().Be(targetUri); request.Headers.GetValues("x-ms-request-test").Should().BeEquivalentTo("request"); request.Content.Should().BeNull(); var response = new HttpResponseMessage(HttpStatusCode.SwitchingProtocols); response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); upstreamStream = new DuplexStream( readStream: StringToStream("response content"), writeStream: new MemoryStream()); response.Content = new RawStreamContent(upstreamStream); return(response); }); var factoryMock = new Mock <IProxyHttpClientFactory>(); factoryMock.Setup(f => f.CreateUpgradableClient()).Returns(client); var proxyTelemetryContext = new ProxyTelemetryContext( backendId: "be1", routeId: "rt1", endpointId: "ep1"); // Act await sut.ProxyAsync(httpContext, targetUri, factoryMock.Object, proxyTelemetryContext, CancellationToken.None, CancellationToken.None); // Assert httpContext.Response.StatusCode.Should().Be(StatusCodes.Status101SwitchingProtocols); httpContext.Response.Headers["x-ms-response-test"].Should().BeEquivalentTo("response"); downstreamStream.WriteStream.Position = 0; var returnedToDownstream = StreamToString(downstreamStream.WriteStream); returnedToDownstream.Should().Be("response content"); upstreamStream.Should().NotBeNull(); upstreamStream.WriteStream.Position = 0; var sentToUpstream = StreamToString(upstreamStream.WriteStream); sentToUpstream.Should().Be("request content"); }
public async Task ProxyAsync_NormalRequest_Works() { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers.Add(":authority", "example.com:3456"); httpContext.Request.Headers.Add("x-ms-request-test", "request"); httpContext.Request.Headers.Add("Content-Language", "requestLanguage"); httpContext.Request.Body = StringToStream("request content"); httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var targetUri = new Uri("https://localhost:123/a/b/api/test"); var sut = Create <HttpProxy>(); var client = MockHttpHandler.CreateClient( async(HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal(targetUri, request.RequestUri); Assert.Contains("request", request.Headers.GetValues("x-ms-request-test")); Assert.Null(request.Headers.Host); Assert.False(request.Headers.TryGetValues(":authority", out var value)); Assert.Equal("127.0.0.1", request.Headers.GetValues("x-forwarded-for").Single()); Assert.Equal("example.com:3456", request.Headers.GetValues("x-forwarded-host").Single()); Assert.Equal("http", request.Headers.GetValues("x-forwarded-proto").Single()); Assert.NotNull(request.Content); Assert.Contains("requestLanguage", request.Content.Headers.GetValues("Content-Language")); var capturedRequestContent = new MemoryStream(); // Use CopyToAsync as this is what HttpClient and friends use internally await request.Content.CopyToAsync(capturedRequestContent); capturedRequestContent.Position = 0; var capturedContentText = StreamToString(capturedRequestContent); Assert.Equal("request content", capturedContentText); var response = new HttpResponseMessage((HttpStatusCode)234); response.ReasonPhrase = "Test Reason Phrase"; response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); response.Content = new StreamContent(StringToStream("response content")); response.Content.Headers.TryAddWithoutValidation("Content-Language", "responseLanguage"); return(response); }); var factoryMock = new Mock <IProxyHttpClientFactory>(); factoryMock.Setup(f => f.CreateNormalClient()).Returns(client); var proxyTelemetryContext = new ProxyTelemetryContext( backendId: "be1", routeId: "rt1", destinationId: "d1"); // Act await sut.ProxyAsync(httpContext, targetUri, factoryMock.Object, proxyTelemetryContext, CancellationToken.None, CancellationToken.None); // Assert Assert.Equal(234, httpContext.Response.StatusCode); var reasonPhrase = httpContext.Features.Get <IHttpResponseFeature>().ReasonPhrase; Assert.Equal("Test Reason Phrase", reasonPhrase); Assert.Contains("response", httpContext.Response.Headers["x-ms-response-test"].ToArray()); Assert.Contains("responseLanguage", httpContext.Response.Headers["Content-Language"].ToArray()); proxyResponseStream.Position = 0; var proxyResponseText = StreamToString(proxyResponseStream); Assert.Equal("response content", proxyResponseText); }
public async Task ProxyAsync_UpgradableRequest_Works() { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers.Add(":authority", "example.com:3456"); httpContext.Request.Headers.Add("x-ms-request-test", "request"); httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; var downstreamStream = new DuplexStream( readStream: StringToStream("request content"), writeStream: new MemoryStream()); DuplexStream upstreamStream = null; var upgradeFeatureMock = new Mock <IHttpUpgradeFeature>(); upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true); upgradeFeatureMock.Setup(u => u.UpgradeAsync()).ReturnsAsync(downstreamStream); httpContext.Features.Set(upgradeFeatureMock.Object); var targetUri = new Uri("https://localhost:123/a/b/api/test?a=b&c=d"); var sut = Create <HttpProxy>(); var client = MockHttpHandler.CreateClient( async(HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(1, 1), request.Version); Assert.Equal(HttpMethod.Get, request.Method); Assert.Equal(targetUri, request.RequestUri); Assert.Contains("request", request.Headers.GetValues("x-ms-request-test")); Assert.Null(request.Headers.Host); Assert.False(request.Headers.TryGetValues(":authority", out var value)); Assert.Equal("127.0.0.1", request.Headers.GetValues("x-forwarded-for").Single()); Assert.Equal("example.com:3456", request.Headers.GetValues("x-forwarded-host").Single()); Assert.Equal("http", request.Headers.GetValues("x-forwarded-proto").Single()); Assert.Null(request.Content); var response = new HttpResponseMessage(HttpStatusCode.SwitchingProtocols); response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); upstreamStream = new DuplexStream( readStream: StringToStream("response content"), writeStream: new MemoryStream()); response.Content = new RawStreamContent(upstreamStream); return(response); }); var factoryMock = new Mock <IProxyHttpClientFactory>(); factoryMock.Setup(f => f.CreateUpgradableClient()).Returns(client); var proxyTelemetryContext = new ProxyTelemetryContext( backendId: "be1", routeId: "rt1", destinationId: "d1"); // Act await sut.ProxyAsync(httpContext, targetUri, factoryMock.Object, proxyTelemetryContext, CancellationToken.None, CancellationToken.None); // Assert Assert.Equal(StatusCodes.Status101SwitchingProtocols, httpContext.Response.StatusCode); Assert.Contains("response", httpContext.Response.Headers["x-ms-response-test"].ToArray()); downstreamStream.WriteStream.Position = 0; var returnedToDownstream = StreamToString(downstreamStream.WriteStream); Assert.Equal("response content", returnedToDownstream); Assert.NotNull(upstreamStream); upstreamStream.WriteStream.Position = 0; var sentToUpstream = StreamToString(upstreamStream.WriteStream); Assert.Equal("request content", sentToUpstream); }
/// <inheritdoc/> public async Task Invoke(HttpContext context) { Contracts.CheckValue(context, nameof(context)); var backend = context.Features.Get <BackendInfo>() ?? throw new InvalidOperationException("Backend unspecified."); var destinations = context.Features.Get <IAvailableDestinationsFeature>()?.Destinations ?? throw new InvalidOperationException("The IAvailableDestinationsFeature Destinations collection was not set."); var routeConfig = context.GetEndpoint()?.Metadata.GetMetadata <RouteConfig>() ?? throw new InvalidOperationException("RouteConfig unspecified."); if (destinations.Count == 0) { Log.NoAvailableDestinations(_logger, backend.BackendId); context.Response.StatusCode = 503; return; } var destination = destinations[0]; if (destinations.Count > 1) { var random = _randomFactory.CreateRandomInstance(); Log.MultipleDestinationsAvailable(_logger, backend.BackendId); destination = destinations[random.Next(destinations.Count)]; } var destinationConfig = destination.Config.Value; if (destinationConfig == null) { throw new InvalidOperationException($"Chosen destination has no configs set: '{destination.DestinationId}'"); } using (var shortCts = CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted)) { // TODO: Configurable timeout, measure from request start, make it unit-testable shortCts.CancelAfter(TimeSpan.FromSeconds(30)); // TODO: Retry against other destinations try { // TODO: Apply caps backend.ConcurrencyCounter.Increment(); destination.ConcurrencyCounter.Increment(); // TODO: Duplex channels should not have a timeout (?), but must react to Proxy force-shutdown signals. var longCancellation = context.RequestAborted; var proxyTelemetryContext = new ProxyTelemetryContext( backendId: backend.BackendId, routeId: routeConfig.Route.RouteId, destinationId: destination.DestinationId); await _operationLogger.ExecuteAsync( "ReverseProxy.Proxy", () => _httpProxy.ProxyAsync(context, destinationConfig.Address, routeConfig.Transforms, backend.ProxyHttpClientFactory, proxyTelemetryContext, shortCancellation: shortCts.Token, longCancellation: longCancellation)); } finally { destination.ConcurrencyCounter.Decrement(); backend.ConcurrencyCounter.Decrement(); } } }
public async Task ProxyAsync_NormalRequestWithExistingForwarders_Appends() { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.PathBase = "/pathbase"; httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers.Add(":authority", "example.com:3456"); httpContext.Request.Headers.Add("x-forwarded-for", "::1"); httpContext.Request.Headers.Add("x-forwarded-proto", "https"); httpContext.Request.Headers.Add("x-forwarded-host", "some.other.host:4567"); httpContext.Request.Headers.Add("x-forwarded-pathbase", "/other"); httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; var transforms = new Transforms(copyRequestHeaders: false, requestTransforms: Array.Empty <RequestParametersTransform>(), requestHeaderTransforms: new Dictionary <string, RequestHeaderTransform>(StringComparer.OrdinalIgnoreCase) { // Defaults { HeaderNames.Host, new RequestHeaderValueTransform(string.Empty, append: false) }, // Default, remove Host { "x-forwarded-for", new RequestHeaderXForwardedForTransform(append: true) }, { "x-forwarded-host", new RequestHeaderXForwardedHostTransform(append: true) }, { "x-forwarded-proto", new RequestHeaderXForwardedProtoTransform(append: true) }, { "x-forwarded-pathbase", new RequestHeaderXForwardedPathBaseTransform(append: true) }, }, responseHeaderTransforms: new Dictionary <string, ResponseHeaderTransform>(StringComparer.OrdinalIgnoreCase), responseTrailerTransforms: new Dictionary <string, ResponseHeaderTransform>(StringComparer.OrdinalIgnoreCase)); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/a/b/"; var targetUri = "https://localhost:123/a/b/api/test?a=b&c=d"; var sut = Create <HttpProxy>(); var client = MockHttpHandler.CreateClient( async(HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(HttpMethod.Get, request.Method); Assert.Equal(targetUri, request.RequestUri.AbsoluteUri); Assert.Equal(new[] { "::1", "127.0.0.1" }, request.Headers.GetValues("x-forwarded-for")); Assert.Equal(new[] { "https", "http" }, request.Headers.GetValues("x-forwarded-proto")); Assert.Equal(new[] { "some.other.host:4567", "example.com:3456" }, request.Headers.GetValues("x-forwarded-host")); Assert.Equal(new[] { "/other", "/pathbase" }, request.Headers.GetValues("x-forwarded-pathbase")); Assert.Null(request.Headers.Host); Assert.False(request.Headers.TryGetValues(":authority", out var value)); // The proxy throws if the request body is not read. await request.Content.CopyToAsync(Stream.Null); var response = new HttpResponseMessage((HttpStatusCode)234); return(response); }); var factoryMock = new Mock <IProxyHttpClientFactory>(); factoryMock.Setup(f => f.CreateNormalClient()).Returns(client); var proxyTelemetryContext = new ProxyTelemetryContext( backendId: "be1", routeId: "rt1", destinationId: "d1"); // Act await sut.ProxyAsync(httpContext, destinationPrefix, transforms, factoryMock.Object, proxyTelemetryContext, CancellationToken.None, CancellationToken.None); // Assert Assert.Equal(234, httpContext.Response.StatusCode); }
public async Task ProxyAsync_NormalRequestWithCopyRequestHeadersDisabled_Works() { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.PathBase = "/api"; httpContext.Request.Path = "/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers.Add(":authority", "example.com:3456"); httpContext.Request.Headers.Add("x-ms-request-test", "request"); httpContext.Request.Headers.Add("Content-Language", "requestLanguage"); httpContext.Request.Body = StringToStream("request content"); httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/a/b/"; var transforms = new Transforms(copyRequestHeaders: false, requestTransforms: Array.Empty <RequestParametersTransform>(), requestHeaderTransforms: new Dictionary <string, RequestHeaderTransform>(StringComparer.OrdinalIgnoreCase) { { "transformHeader", new RequestHeaderValueTransform("value", append: false) }, { "x-ms-request-test", new RequestHeaderValueTransform("transformValue", append: true) }, // Defaults { "x-forwarded-for", new RequestHeaderXForwardedForTransform(append: true) }, { "x-forwarded-host", new RequestHeaderXForwardedHostTransform(append: true) }, { "x-forwarded-proto", new RequestHeaderXForwardedProtoTransform(append: true) }, { "x-forwarded-pathbase", new RequestHeaderXForwardedPathBaseTransform(append: true) }, }, responseHeaderTransforms: new Dictionary <string, ResponseHeaderTransform>(StringComparer.OrdinalIgnoreCase), responseTrailerTransforms: new Dictionary <string, ResponseHeaderTransform>(StringComparer.OrdinalIgnoreCase)); var targetUri = "https://localhost:123/a/b/test?a=b&c=d"; var sut = Create <HttpProxy>(); var client = MockHttpHandler.CreateClient( async(HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal(targetUri, request.RequestUri.AbsoluteUri); Assert.Equal(new[] { "value" }, request.Headers.GetValues("transformHeader")); Assert.Equal(new[] { "request", "transformValue" }, request.Headers.GetValues("x-ms-request-test")); Assert.Null(request.Headers.Host); Assert.False(request.Headers.TryGetValues(":authority", out var _)); Assert.Equal("127.0.0.1", request.Headers.GetValues("x-forwarded-for").Single()); Assert.Equal("example.com:3456", request.Headers.GetValues("x-forwarded-host").Single()); Assert.Equal("http", request.Headers.GetValues("x-forwarded-proto").Single()); Assert.Equal("/api", request.Headers.GetValues("x-forwarded-pathbase").Single()); Assert.NotNull(request.Content); Assert.False(request.Content.Headers.TryGetValues("Content-Language", out var _)); var capturedRequestContent = new MemoryStream(); // Use CopyToAsync as this is what HttpClient and friends use internally await request.Content.CopyToAsync(capturedRequestContent); capturedRequestContent.Position = 0; var capturedContentText = StreamToString(capturedRequestContent); Assert.Equal("request content", capturedContentText); var response = new HttpResponseMessage((HttpStatusCode)234); response.ReasonPhrase = "Test Reason Phrase"; response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); response.Content = new StreamContent(StringToStream("response content")); response.Content.Headers.TryAddWithoutValidation("Content-Language", "responseLanguage"); return(response); }); var factoryMock = new Mock <IProxyHttpClientFactory>(); factoryMock.Setup(f => f.CreateNormalClient()).Returns(client); var proxyTelemetryContext = new ProxyTelemetryContext( backendId: "be1", routeId: "rt1", destinationId: "d1"); // Act await sut.ProxyAsync(httpContext, destinationPrefix, transforms : transforms, factoryMock.Object, proxyTelemetryContext, CancellationToken.None, CancellationToken.None); // Assert Assert.Equal(234, httpContext.Response.StatusCode); var reasonPhrase = httpContext.Features.Get <IHttpResponseFeature>().ReasonPhrase; Assert.Equal("Test Reason Phrase", reasonPhrase); Assert.Contains("response", httpContext.Response.Headers["x-ms-response-test"].ToArray()); Assert.Contains("responseLanguage", httpContext.Response.Headers["Content-Language"].ToArray()); proxyResponseStream.Position = 0; var proxyResponseText = StreamToString(proxyResponseStream); Assert.Equal("response content", proxyResponseText); }
/// <inheritdoc/> public async Task Invoke(HttpContext context) { _ = context ?? throw new ArgumentNullException(nameof(context)); var reverseProxyFeature = context.GetRequiredProxyFeature(); var destinations = reverseProxyFeature.AvailableDestinations ?? throw new InvalidOperationException($"The {nameof(IReverseProxyFeature)} Destinations collection was not set."); var routeConfig = context.GetRequiredRouteConfig(); var cluster = routeConfig.Cluster; if (destinations.Count == 0) { Log.NoAvailableDestinations(_logger, cluster.ClusterId); context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; context.Features.Set <IProxyErrorFeature>(new ProxyErrorFeature(ProxyError.NoAvailableDestinations, ex: null)); return; } var destination = destinations[0]; if (destinations.Count > 1) { var random = _randomFactory.CreateRandomInstance(); Log.MultipleDestinationsAvailable(_logger, cluster.ClusterId); destination = destinations[random.Next(destinations.Count)]; } var destinationConfig = destination.Config; if (destinationConfig == null) { throw new InvalidOperationException($"Chosen destination has no configs set: '{destination.DestinationId}'"); } // TODO: Make this configurable on a route rather than create it per request? var proxyOptions = new RequestProxyOptions() { Transforms = routeConfig.Transforms, }; try { cluster.ConcurrencyCounter.Increment(); destination.ConcurrencyCounter.Increment(); var proxyTelemetryContext = new ProxyTelemetryContext( clusterId: cluster.ClusterId, routeId: routeConfig.Route.RouteId, destinationId: destination.DestinationId); await _operationLogger.ExecuteAsync( "ReverseProxy.Proxy", () => _httpProxy.ProxyAsync(context, destinationConfig.Address, reverseProxyFeature.ClusterConfig.HttpClient, proxyOptions, proxyTelemetryContext)); } finally { destination.ConcurrencyCounter.Decrement(); cluster.ConcurrencyCounter.Decrement(); } }
/// <inheritdoc/> public async Task Invoke(HttpContext context) { Contracts.CheckValue(context, nameof(context)); var backend = context.Features.Get <BackendInfo>() ?? throw new InvalidOperationException("Backend unspecified."); var endpoints = context.Features.Get <IAvailableBackendEndpointsFeature>()?.Endpoints ?? throw new InvalidOperationException("The AvailableBackendEndpoints collection was not set."); var routeConfig = context.GetEndpoint()?.Metadata.GetMetadata <RouteConfig>() ?? throw new InvalidOperationException("RouteConfig unspecified."); if (endpoints.Count == 0) { _logger.LogWarning("No available endpoints."); context.Response.StatusCode = 503; return; } var endpoint = endpoints[0]; if (endpoints.Count > 1) { _logger.LogWarning("More than one endpoint available, load balancing may not be configured correctly. Choosing randomly."); endpoint = endpoints[_random.Next(endpoints.Count)]; } var endpointConfig = endpoint.Config.Value; if (endpointConfig == null) { throw new InvalidOperationException($"Chosen endpoint has no configs set: '{endpoint.EndpointId}'"); } // TODO: support StripPrefix and other url transformations var targetUrl = BuildOutgoingUrl(context, endpointConfig.Address); _logger.LogInformation($"Proxying to {targetUrl}"); var targetUri = new Uri(targetUrl, UriKind.Absolute); using (var shortCts = CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted)) { // TODO: Configurable timeout, measure from request start, make it unit-testable shortCts.CancelAfter(TimeSpan.FromSeconds(30)); // TODO: Retry against other endpoints try { // TODO: Apply caps backend.ConcurrencyCounter.Increment(); endpoint.ConcurrencyCounter.Increment(); // TODO: Duplex channels should not have a timeout (?), but must react to Proxy force-shutdown signals. var longCancellation = context.RequestAborted; var proxyTelemetryContext = new ProxyTelemetryContext( backendId: backend.BackendId, routeId: routeConfig.Route.RouteId, endpointId: endpoint.EndpointId); await _operationLogger.ExecuteAsync( "ReverseProxy.Proxy", () => _httpProxy.ProxyAsync(context, targetUri, backend.ProxyHttpClientFactory, proxyTelemetryContext, shortCancellation: shortCts.Token, longCancellation: longCancellation)); } finally { endpoint.ConcurrencyCounter.Decrement(); backend.ConcurrencyCounter.Decrement(); } } }
public async Task ProxyAsync_UpgradableRequestFailsToUpgrade_ProxiesResponse() { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "https"; httpContext.Request.Host = new HostString("example.com"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers.Add(":host", "example.com"); httpContext.Request.Headers.Add("x-ms-request-test", "request"); // TODO: https://github.com/microsoft/reverse-proxy/issues/255 httpContext.Request.Headers.Add("Upgrade", "WebSocket"); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var upgradeFeatureMock = new Mock <IHttpUpgradeFeature>(MockBehavior.Strict); upgradeFeatureMock.SetupGet(u => u.IsUpgradableRequest).Returns(true); httpContext.Features.Set(upgradeFeatureMock.Object); var destinationPrefix = "https://localhost:123/a/b/"; var targetUri = "https://localhost:123/a/b/api/test?a=b&c=d"; var sut = Create <HttpProxy>(); var client = MockHttpHandler.CreateClient( async(HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(1, 1), request.Version); Assert.Equal(HttpMethod.Get, request.Method); Assert.Equal(targetUri, request.RequestUri.AbsoluteUri); Assert.Contains("request", request.Headers.GetValues("x-ms-request-test")); Assert.Null(request.Content); var response = new HttpResponseMessage((HttpStatusCode)234); response.ReasonPhrase = "Test Reason Phrase"; response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); response.Content = new StreamContent(StringToStream("response content")); response.Content.Headers.TryAddWithoutValidation("Content-Language", "responseLanguage"); return(response); }); var factoryMock = new Mock <IProxyHttpClientFactory>(); factoryMock.Setup(f => f.CreateClient()).Returns(client); var proxyTelemetryContext = new ProxyTelemetryContext( clusterId: "be1", routeId: "rt1", destinationId: "d1"); // Act await sut.ProxyAsync(httpContext, destinationPrefix, Transforms.Empty, factoryMock.Object, proxyTelemetryContext, CancellationToken.None, CancellationToken.None); // Assert Assert.Equal(234, httpContext.Response.StatusCode); var reasonPhrase = httpContext.Features.Get <IHttpResponseFeature>().ReasonPhrase; Assert.Equal("Test Reason Phrase", reasonPhrase); Assert.Contains("response", httpContext.Response.Headers["x-ms-response-test"].ToArray()); Assert.Contains("responseLanguage", httpContext.Response.Headers["Content-Language"].ToArray()); proxyResponseStream.Position = 0; var proxyResponseText = StreamToString(proxyResponseStream); Assert.Equal("response content", proxyResponseText); }
/// <inheritdoc/> public async Task Invoke(HttpContext context) { _ = context ?? throw new ArgumentNullException(nameof(context)); var reverseProxyFeature = context.GetRequiredProxyFeature(); var destinations = reverseProxyFeature.AvailableDestinations ?? throw new InvalidOperationException($"The {nameof(IReverseProxyFeature)} Destinations collection was not set."); var routeConfig = context.GetRequiredRouteConfig(); var cluster = routeConfig.Cluster; if (destinations.Count == 0) { Log.NoAvailableDestinations(_logger, cluster.ClusterId); context.Response.StatusCode = 503; return; } var destination = destinations[0]; if (destinations.Count > 1) { var random = _randomFactory.CreateRandomInstance(); Log.MultipleDestinationsAvailable(_logger, cluster.ClusterId); destination = destinations[random.Next(destinations.Count)]; } var destinationConfig = destination.Config; if (destinationConfig == null) { throw new InvalidOperationException($"Chosen destination has no configs set: '{destination.DestinationId}'"); } using (var shortCts = CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted)) { // TODO: Configurable timeout, measure from request start, make it unit-testable shortCts.CancelAfter(TimeSpan.FromSeconds(30)); // TODO: Retry against other destinations try { // TODO: Apply caps cluster.ConcurrencyCounter.Increment(); destination.ConcurrencyCounter.Increment(); // TODO: Duplex channels should not have a timeout (?), but must react to Proxy force-shutdown signals. var longCancellation = context.RequestAborted; var proxyTelemetryContext = new ProxyTelemetryContext( clusterId: cluster.ClusterId, routeId: routeConfig.Route.RouteId, destinationId: destination.DestinationId); await _operationLogger.ExecuteAsync( "ReverseProxy.Proxy", () => _httpProxy.ProxyAsync(context, destinationConfig.Address, routeConfig.Transforms, reverseProxyFeature.ClusterConfig.HttpClient, proxyTelemetryContext, shortCancellation: shortCts.Token, longCancellation: longCancellation)); } finally { destination.ConcurrencyCounter.Decrement(); cluster.ConcurrencyCounter.Decrement(); } } }
public async Task ProxyAsync_NormalRequestWithTransforms_Works() { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "POST"; httpContext.Request.Protocol = "http/2"; httpContext.Request.Scheme = "http"; httpContext.Request.Host = new HostString("example.com:3456"); httpContext.Request.Path = "/path/base/dropped"; httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); httpContext.Request.Headers.Add(":authority", "example.com:3456"); httpContext.Request.Headers.Add("x-ms-request-test", "request"); httpContext.Request.Headers.Add("Content-Language", "requestLanguage"); httpContext.Request.Body = StringToStream("request content"); httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; httpContext.Features.Set <IHttpResponseTrailersFeature>(new TestTrailersFeature()); var proxyResponseStream = new MemoryStream(); httpContext.Response.Body = proxyResponseStream; var destinationPrefix = "https://localhost:123/a/b/"; var transforms = new Transforms(copyRequestHeaders: true, requestTransforms: new[] { new PathStringTransform(PathStringTransform.PathTransformMode.Prefix, "/prefix"), }, requestHeaderTransforms: new Dictionary <string, RequestHeaderTransform>(StringComparer.OrdinalIgnoreCase) { { "transformHeader", new RequestHeaderValueTransform("value", append: false) }, { "x-ms-request-test", new RequestHeaderValueTransform("transformValue", append: true) }, { HeaderNames.Host, new RequestHeaderValueTransform(string.Empty, append: false) } // Default, remove Host }, responseHeaderTransforms: new Dictionary <string, ResponseHeaderTransform>(StringComparer.OrdinalIgnoreCase) { { "transformHeader", new ResponseHeaderValueTransform("value", append: false, always: true) }, { "x-ms-response-test", new ResponseHeaderValueTransform("value", append: true, always: false) } }, responseTrailerTransforms: new Dictionary <string, ResponseHeaderTransform>(StringComparer.OrdinalIgnoreCase) { { "trailerTransform", new ResponseHeaderValueTransform("value", append: false, always: true) } }); var targetUri = "https://localhost:123/a/b/prefix/api/test?a=b&c=d"; var sut = Create <HttpProxy>(); var client = MockHttpHandler.CreateClient( async(HttpRequestMessage request, CancellationToken cancellationToken) => { await Task.Yield(); Assert.Equal(new Version(2, 0), request.Version); Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal(targetUri, request.RequestUri.AbsoluteUri); Assert.Equal(new[] { "value" }, request.Headers.GetValues("transformHeader")); Assert.Equal(new[] { "request", "transformValue" }, request.Headers.GetValues("x-ms-request-test")); Assert.Null(request.Headers.Host); Assert.False(request.Headers.TryGetValues(":authority", out var value)); Assert.NotNull(request.Content); Assert.Contains("requestLanguage", request.Content.Headers.GetValues("Content-Language")); var capturedRequestContent = new MemoryStream(); // Use CopyToAsync as this is what HttpClient and friends use internally await request.Content.CopyToAsync(capturedRequestContent); capturedRequestContent.Position = 0; var capturedContentText = StreamToString(capturedRequestContent); Assert.Equal("request content", capturedContentText); var response = new HttpResponseMessage((HttpStatusCode)234); response.ReasonPhrase = "Test Reason Phrase"; response.Headers.TryAddWithoutValidation("x-ms-response-test", "response"); response.Content = new StreamContent(StringToStream("response content")); response.Content.Headers.TryAddWithoutValidation("Content-Language", "responseLanguage"); return(response); }); var proxyTelemetryContext = new ProxyTelemetryContext( clusterId: "be1", routeId: "rt1", destinationId: "d1"); // Act await sut.ProxyAsync(httpContext, destinationPrefix, transforms : transforms, client, proxyTelemetryContext, CancellationToken.None, CancellationToken.None); // Assert Assert.Equal(234, httpContext.Response.StatusCode); var reasonPhrase = httpContext.Features.Get <IHttpResponseFeature>().ReasonPhrase; Assert.Equal("Test Reason Phrase", reasonPhrase); Assert.Equal(new[] { "response", "value" }, httpContext.Response.Headers["x-ms-response-test"].ToArray()); Assert.Contains("responseLanguage", httpContext.Response.Headers["Content-Language"].ToArray()); Assert.Contains("value", httpContext.Response.Headers["transformHeader"].ToArray()); Assert.Equal(new[] { "value" }, httpContext.Features.Get <IHttpResponseTrailersFeature>().Trailers?["trailerTransform"].ToArray()); proxyResponseStream.Position = 0; var proxyResponseText = StreamToString(proxyResponseStream); Assert.Equal("response content", proxyResponseText); }