Ejemplo n.º 1
0
        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);
        }
Ejemplo n.º 2
0
        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);
        }
Ejemplo n.º 3
0
        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");
        }
Ejemplo n.º 4
0
        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");
        }
Ejemplo n.º 5
0
        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");
        }
Ejemplo n.º 6
0
        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);
        }
Ejemplo n.º 7
0
        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);
        }
Ejemplo n.º 8
0
        /// <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();
                }
            }
        }
Ejemplo n.º 9
0
        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);
        }
Ejemplo n.º 10
0
        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();
            }
        }
Ejemplo n.º 12
0
        /// <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();
                }
            }
        }
Ejemplo n.º 13
0
        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();
                }
            }
        }
Ejemplo n.º 15
0
        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);
        }