public async Task SerializeToStreamAsync_RespectsContentCancellation()
    {
        var tcs = new TaskCompletionSource <byte>(TaskCreationOptions.RunContinuationsAsynchronously);

        var source = new ReadDelegatingStream(new MemoryStream(), async(buffer, cancellation) =>
        {
            if (buffer.Length == 0)
            {
                return(0);
            }

            Assert.False(cancellation.IsCancellationRequested);
            await tcs.Task;
            Assert.True(cancellation.IsCancellationRequested);
            return(0);
        });

        using var contentCts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);

        var sut = CreateContent(source, contentCancellation: contentCts);

        var copyToTask = sut.CopyToWithCancellationAsync(new MemoryStream());

        contentCts.Cancel();
        tcs.SetResult(0);

        await copyToTask;
    }
Ejemplo n.º 2
0
    public async Task SlowStreams_TelemetryReportsCorrectTime(bool isRequest)
    {
        var events = TestEventListener.Collect();

        const int SourceSize  = 3;
        var       sourceBytes = new byte[SourceSize];
        var       source      = new MemoryStream(sourceBytes);
        var       destination = new MemoryStream();

        var clock               = new ManualClock();
        var sourceWaitTime      = TimeSpan.FromMilliseconds(12345);
        var destinationWaitTime = TimeSpan.FromMilliseconds(42);

        using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);
        await StreamCopier.CopyAsync(
            isRequest,
            new SlowStream(source, clock, sourceWaitTime),
            new SlowStream(destination, clock, destinationWaitTime),
            SourceSize,
            clock,
            cts,
            cts.Token);

        Assert.Equal(sourceBytes, destination.ToArray());

        AssertContentTransferred(events, isRequest, SourceSize,
                                 iops: SourceSize + 1,
                                 firstReadTime: sourceWaitTime,
                                 readTime: (SourceSize + 1) * sourceWaitTime,
                                 writeTime: SourceSize * destinationWaitTime);
    }
Ejemplo n.º 3
0
    public async Task DestinationThrows_Reported(bool isRequest)
    {
        var events = TestEventListener.Collect();

        const int SourceSize   = 10;
        const int BytesPerRead = 3;

        var clock               = new ManualClock();
        var sourceWaitTime      = TimeSpan.FromMilliseconds(12345);
        var destinationWaitTime = TimeSpan.FromMilliseconds(42);
        var source              = new SlowStream(new MemoryStream(new byte[SourceSize]), clock, sourceWaitTime)
        {
            MaxBytesPerRead = BytesPerRead
        };
        var destination = new SlowStream(new ThrowStream(), clock, destinationWaitTime);

        using var cts      = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);
        var(result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, SourceSize, clock, cts, cts.Token);

        Assert.Equal(StreamCopyResult.OutputError, result);
        Assert.IsAssignableFrom <IOException>(error);

        AssertContentTransferred(events, isRequest,
                                 contentLength: BytesPerRead,
                                 iops: 1,
                                 firstReadTime: sourceWaitTime,
                                 readTime: sourceWaitTime,
                                 writeTime: destinationWaitTime);
    }
    private static StreamCopyHttpContent CreateContent(HttpRequest request = null, bool autoFlushHttpClientOutgoingStream = false, IClock clock = null, ActivityCancellationTokenSource contentCancellation = null)
    {
        request ??= new DefaultHttpContext().Request;
        clock ??= new Clock();

        contentCancellation ??= ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);

        return(new StreamCopyHttpContent(request, autoFlushHttpClientOutgoingStream, clock, contentCancellation));
    }
    private static StreamCopyHttpContent CreateContent(Stream source = null, bool autoFlushHttpClientOutgoingStream = false, IClock clock = null, ActivityCancellationTokenSource contentCancellation = null)
    {
        source ??= new MemoryStream();
        clock ??= new Clock();

        contentCancellation ??= ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);

        return(new StreamCopyHttpContent(source, autoFlushHttpClientOutgoingStream, clock, contentCancellation));
    }
Ejemplo n.º 6
0
    public void ActivityCancellationTokenSource_RespectsLinkedToken()
    {
        var linkedCts = new CancellationTokenSource();

        var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), linkedCts.Token);

        linkedCts.Cancel();

        Assert.True(cts.IsCancellationRequested);
    }
Ejemplo n.º 7
0
    public void ActivityCancellationTokenSource_DoesNotPoolsCanceledSources()
    {
        var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);

        cts.Cancel();

        var cts2 = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);

        Assert.NotSame(cts, cts2);
    }
Ejemplo n.º 8
0
    public void ActivityCancellationTokenSource_ClearsRegistrations()
    {
        var linkedCts = new CancellationTokenSource();

        var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), linkedCts.Token);

        cts.Return();

        linkedCts.Cancel();

        Assert.False(cts.IsCancellationRequested);
    }
Ejemplo n.º 9
0
    public async Task ActivityCancellationTokenSource_RespectsTimeout()
    {
        var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromMilliseconds(1), CancellationToken.None);

        for (var i = 0; i < 1000; i++)
        {
            if (cts.IsCancellationRequested)
            {
                return;
            }

            await Task.Delay(1);
        }

        Assert.True(false, "Cts was not canceled");
    }
Ejemplo n.º 10
0
    private async ValueTask <(StreamCopyResult, Exception?)> CopyResponseBodyAsync(HttpContent destinationResponseContent, Stream clientResponseStream,
                                                                                   ActivityCancellationTokenSource activityCancellationSource)
    {
        // SocketHttpHandler and similar transports always provide an HttpContent object, even if it's empty.
        // In 3.1 this is only likely to return null in tests.
        // As of 5.0 HttpResponse.Content never returns null.
        // https://github.com/dotnet/runtime/blame/8fc68f626a11d646109a758cb0fc70a0aa7826f1/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs#L46
        if (destinationResponseContent != null)
        {
            using var destinationResponseStream = await destinationResponseContent.ReadAsStreamAsync();

            // The response content-length is enforced by the server.
            return(await StreamCopier.CopyAsync(isRequest : false, destinationResponseStream, clientResponseStream, StreamCopier.UnknownLength, _clock, activityCancellationSource, activityCancellationSource.Token));
        }

        return(StreamCopyResult.Success, null);
    }
Ejemplo n.º 11
0
    public async Task CopyAsync_Works(bool isRequest)
    {
        var events = TestEventListener.Collect();

        const int SourceSize  = (128 * 1024) - 3;
        var       sourceBytes = Enumerable.Range(0, SourceSize).Select(i => (byte)(i % 256)).ToArray();
        var       source      = new MemoryStream(sourceBytes);
        var       destination = new MemoryStream();

        using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);
        await StreamCopier.CopyAsync(isRequest, source, destination, SourceSize, new Clock(), cts, cts.Token);

        Assert.False(cts.Token.IsCancellationRequested);

        Assert.Equal(sourceBytes, destination.ToArray());

        AssertContentTransferred(events, isRequest, SourceSize);
    }
Ejemplo n.º 12
0
    public void ActivityCancellationTokenSource_PoolsSources()
    {
        // This test can run in parallel with others making use of ActivityCancellationTokenSource
        // A different thread could have already added/removed a source from the queue

        for (var i = 0; i < 1000; i++)
        {
            var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);
            cts.Return();

            var cts2 = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);
            cts2.Return();

            if (ReferenceEquals(cts, cts2))
            {
                return;
            }
        }

        Assert.True(false, "CancellationTokenSources were not pooled");
    }
Ejemplo n.º 13
0
    public async Task Cancelled_Reported(bool isRequest)
    {
        var events = TestEventListener.Collect();

        var source      = new MemoryStream(new byte[10]);
        var destination = new MemoryStream();

        using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);
        cts.Cancel();
        var(result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, StreamCopier.UnknownLength, new ManualClock(), cts, cts.Token);

        Assert.Equal(StreamCopyResult.Canceled, result);
        Assert.IsAssignableFrom <OperationCanceledException>(error);

        AssertContentTransferred(events, isRequest,
                                 contentLength: 0,
                                 iops: 1,
                                 firstReadTime: TimeSpan.Zero,
                                 readTime: TimeSpan.Zero,
                                 writeTime: TimeSpan.Zero);
    }
Ejemplo n.º 14
0
    public async Task SourceThrows_Reported(bool isRequest)
    {
        var events = TestEventListener.Collect();

        var clock          = new ManualClock();
        var sourceWaitTime = TimeSpan.FromMilliseconds(12345);
        var source         = new SlowStream(new ThrowStream(), clock, sourceWaitTime);
        var destination    = new MemoryStream();

        using var cts      = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);
        var(result, error) = await StreamCopier.CopyAsync(isRequest, source, destination, StreamCopier.UnknownLength, clock, cts, cts.Token);

        Assert.Equal(StreamCopyResult.InputError, result);
        Assert.IsAssignableFrom <IOException>(error);

        AssertContentTransferred(events, isRequest,
                                 contentLength: 0,
                                 iops: 1,
                                 firstReadTime: sourceWaitTime,
                                 readTime: sourceWaitTime,
                                 writeTime: TimeSpan.Zero);
    }
Ejemplo n.º 15
0
    /// <summary>
    /// Proxies the incoming request to the destination server, and the response back to the client.
    /// </summary>
    /// <remarks>
    /// In what follows, as well as throughout in Reverse Proxy, we consider
    /// the following picture as illustrative of the Proxy.
    /// <code>
    ///      +-------------------+
    ///      |  Destination      +
    ///      +-------------------+
    ///            ▲       |
    ///        (b) |       | (c)
    ///            |       ▼
    ///      +-------------------+
    ///      |      Proxy        +
    ///      +-------------------+
    ///            ▲       |
    ///        (a) |       | (d)
    ///            |       ▼
    ///      +-------------------+
    ///      | Client            +
    ///      +-------------------+
    /// </code>
    ///
    /// (a) and (b) show the *request* path, going from the client to the target.
    /// (c) and (d) show the *response* path, going from the destination back to the client.
    ///
    /// Normal proxying comprises the following steps:
    ///    (0) Disable ASP .NET Core limits for streaming requests
    ///    (1) Create outgoing HttpRequestMessage
    ///    (2) Setup copy of request body (background)             Client --► Proxy --► Destination
    ///    (3) Copy request headers                                Client --► Proxy --► Destination
    ///    (4) Send the outgoing request using HttpMessageInvoker  Client --► Proxy --► Destination
    ///    (5) Copy response status line                           Client ◄-- Proxy ◄-- Destination
    ///    (6) Copy response headers                               Client ◄-- Proxy ◄-- Destination
    ///    (7-A) Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol.
    ///        (7-A-1)  Upgrade client channel                     Client ◄--- Proxy ◄--- Destination
    ///        (7-A-2)  Copy duplex streams and return             Client ◄--► Proxy ◄--► Destination
    ///    (7-B) Copy (normal) response body                       Client ◄-- Proxy ◄-- Destination
    ///    (8) Copy response trailer headers and finish response   Client ◄-- Proxy ◄-- Destination
    ///    (9) Wait for completion of step 2: copying request body Client --► Proxy --► Destination
    ///
    /// ASP .NET Core (Kestrel) will finally send response trailers (if any)
    /// after we complete the steps above and relinquish control.
    /// </remarks>
    public async ValueTask <ForwarderError> SendAsync(
        HttpContext context,
        string destinationPrefix,
        HttpMessageInvoker httpClient,
        ForwarderRequestConfig requestConfig,
        HttpTransformer transformer)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));
        _ = destinationPrefix ?? throw new ArgumentNullException(nameof(destinationPrefix));
        _ = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        _ = requestConfig ?? throw new ArgumentNullException(nameof(requestConfig));
        _ = transformer ?? throw new ArgumentNullException(nameof(transformer));

        // HttpClient overload for SendAsync changes response behavior to fully buffered which impacts performance
        // See discussion in https://github.com/microsoft/reverse-proxy/issues/458
        if (httpClient is HttpClient)
        {
            throw new ArgumentException($"The http client must be of type HttpMessageInvoker, not HttpClient", nameof(httpClient));
        }

        ForwarderTelemetry.Log.ForwarderStart(destinationPrefix);

        var activityCancellationSource = ActivityCancellationTokenSource.Rent(requestConfig?.ActivityTimeout ?? DefaultTimeout, context.RequestAborted);

        try
        {
            var isClientHttp2 = ProtocolHelper.IsHttp2(context.Request.Protocol);

            // NOTE: We heuristically assume gRPC-looking requests may require streaming semantics.
            // See https://github.com/microsoft/reverse-proxy/issues/118 for design discussion.
            var isStreamingRequest = isClientHttp2 && ProtocolHelper.IsGrpcContentType(context.Request.ContentType);

            // :: Step 1-3: Create outgoing HttpRequestMessage
            var(destinationRequest, requestContent) = await CreateRequestMessageAsync(
                context, destinationPrefix, transformer, requestConfig, isStreamingRequest, activityCancellationSource);

            // :: Step 4: Send the outgoing request using HttpClient
            HttpResponseMessage destinationResponse;
            try
            {
                ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.SendAsyncStart);
                destinationResponse = await httpClient.SendAsync(destinationRequest, activityCancellationSource.Token);

                ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.SendAsyncStop);

                // Reset the timeout since we received the response headers.
                activityCancellationSource.ResetTimeout();
            }
            catch (Exception requestException)
            {
                return(await HandleRequestFailureAsync(context, requestContent, requestException, transformer, activityCancellationSource));
            }

            // Detect connection downgrade, which may be problematic for e.g. gRPC.
            if (isClientHttp2 && destinationResponse.Version.Major != 2)
            {
                // TODO: Do something on connection downgrade...
                Log.HttpDowngradeDetected(_logger);
            }

            try
            {
                // :: Step 5: Copy response status line Client ◄-- Proxy ◄-- Destination
                // :: Step 6: Copy response headers Client ◄-- Proxy ◄-- Destination
                var copyBody = await CopyResponseStatusAndHeadersAsync(destinationResponse, context, transformer);

                if (!copyBody)
                {
                    // The transforms callback decided that the response body should be discarded.
                    destinationResponse.Dispose();

                    if (requestContent is not null && requestContent.InProgress)
                    {
                        activityCancellationSource.Cancel();
                        await requestContent.ConsumptionTask;
                    }

                    return(ForwarderError.None);
                }
            }
            catch (Exception ex)
            {
                destinationResponse.Dispose();

                if (requestContent is not null && requestContent.InProgress)
                {
                    activityCancellationSource.Cancel();
                    await requestContent.ConsumptionTask;
                }

                ReportProxyError(context, ForwarderError.ResponseHeaders, ex);
                // Clear the response since status code, reason and some headers might have already been copied and we want clean 502 response.
                context.Response.Clear();
                context.Response.StatusCode = StatusCodes.Status502BadGateway;
                return(ForwarderError.ResponseHeaders);
            }

            // :: Step 7-A: Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol.
            if (destinationResponse.StatusCode == HttpStatusCode.SwitchingProtocols)
            {
                Debug.Assert(requestContent?.Started != true);
                return(await HandleUpgradedResponse(context, destinationResponse, activityCancellationSource));
            }

            // NOTE: it may *seem* wise to call `context.Response.StartAsync()` at this point
            // since it looks like we are ready to send back response headers
            // (and this might help reduce extra delays while we wait to receive the body from the destination).
            // HOWEVER, this would produce the wrong result if it turns out that there is no content
            // from the destination -- instead of sending headers and terminating the stream at once,
            // we would send headers thinking a body may be coming, and there is none.
            // This is problematic on gRPC connections when the destination server encounters an error,
            // in which case it immediately returns the response headers and trailing headers, but no content,
            // and clients misbehave if the initial headers response does not indicate stream end.

            // :: Step 7-B: Copy response body Client ◄-- Proxy ◄-- Destination
            var(responseBodyCopyResult, responseBodyException) = await CopyResponseBodyAsync(destinationResponse.Content, context.Response.Body, activityCancellationSource);

            if (responseBodyCopyResult != StreamCopyResult.Success)
            {
                return(await HandleResponseBodyErrorAsync(context, requestContent, responseBodyCopyResult, responseBodyException !, activityCancellationSource));
            }

            // :: Step 8: Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination
            await CopyResponseTrailingHeadersAsync(destinationResponse, context, transformer);

            if (isStreamingRequest)
            {
                // NOTE: We must call `CompleteAsync` so that Kestrel will flush all bytes to the client.
                // In the case where there was no response body,
                // this is also when headers and trailing headers are sent to the client.
                // Without this, the client might wait forever waiting for response bytes,
                // while we might wait forever waiting for request bytes,
                // leading to a stuck connection and no way to make progress.
                await context.Response.CompleteAsync();
            }

            // :: Step 9: Wait for completion of step 2: copying request body Client --► Proxy --► Destination
            // NOTE: It is possible for the request body to NOT be copied even when there was an incoming requet body,
            // e.g. when the request includes header `Expect: 100-continue` and the destination produced a non-1xx response.
            // We must only wait for the request body to complete if it actually started,
            // otherwise we run the risk of waiting indefinitely for a task that will never complete.
            if (requestContent is not null && requestContent.Started)
            {
                var(requestBodyCopyResult, requestBodyException) = await requestContent.ConsumptionTask;

                if (requestBodyCopyResult != StreamCopyResult.Success)
                {
                    // The response succeeded. If there was a request body error then it was probably because the client or destination decided
                    // to cancel it. Report as low severity.

                    var error = requestBodyCopyResult switch
                    {
                        StreamCopyResult.InputError => ForwarderError.RequestBodyClient,
                        StreamCopyResult.OutputError => ForwarderError.RequestBodyDestination,
                        StreamCopyResult.Canceled => ForwarderError.RequestBodyCanceled,
                        _ => throw new NotImplementedException(requestBodyCopyResult.ToString())
                    };
                    ReportProxyError(context, error, requestBodyException !);
                    return(error);
                }
            }
        }
        finally
        {
            activityCancellationSource.Return();
            ForwarderTelemetry.Log.ForwarderStop(context.Response.StatusCode);
        }

        return(ForwarderError.None);
    }
Ejemplo n.º 16
0
    public async Task LongContentTransfer_TelemetryReportsTransferringEvents(bool isRequest)
    {
        var events = TestEventListener.Collect();

        const int SourceSize  = 123;
        var       sourceBytes = new byte[SourceSize];
        var       source      = new MemoryStream(sourceBytes);
        var       destination = new MemoryStream();

        var clock               = new ManualClock();
        var sourceWaitTime      = TimeSpan.FromMilliseconds(789); // Every second read triggers ContentTransferring
        var destinationWaitTime = TimeSpan.FromMilliseconds(42);

        const int BytesPerRead = 3;
        var       contentReads = (int)Math.Ceiling((double)SourceSize / BytesPerRead);

        using var cts = ActivityCancellationTokenSource.Rent(TimeSpan.FromSeconds(10), CancellationToken.None);
        await StreamCopier.CopyAsync(
            isRequest,
            new SlowStream(source, clock, sourceWaitTime) { MaxBytesPerRead = BytesPerRead },
            new SlowStream(destination, clock, destinationWaitTime),
            SourceSize,
            clock,
            cts,
            cts.Token);

        Assert.Equal(sourceBytes, destination.ToArray());

        AssertContentTransferred(events, isRequest, SourceSize,
                                 iops: contentReads + 1,
                                 firstReadTime: sourceWaitTime,
                                 readTime: (contentReads + 1) * sourceWaitTime,
                                 writeTime: contentReads * destinationWaitTime);

        var transferringEvents = events.Where(e => e.EventName == "ContentTransferring").ToArray();

        Assert.Equal(contentReads / 2, transferringEvents.Length);

        for (var i = 0; i < transferringEvents.Length; i++)
        {
            var payload = transferringEvents[i].Payload;
            Assert.Equal(5, payload.Count);

            Assert.Equal(isRequest, (bool)payload[0]);

            var contentLength = (long)payload[1];

            var iops = (long)payload[2];
            Assert.Equal((i + 1) * 2, iops);

            if (contentLength % BytesPerRead == 0)
            {
                Assert.Equal(iops * BytesPerRead, contentLength);
            }
            else
            {
                Assert.Equal(transferringEvents.Length - 1, i);
                Assert.Equal(SourceSize, contentLength);
            }

            var readTime = new TimeSpan((long)payload[3]);
            Assert.Equal(iops * sourceWaitTime, readTime, new ApproximateTimeSpanComparer());

            var writeTime = new TimeSpan((long)payload[4]);
            Assert.Equal(iops * destinationWaitTime, writeTime, new ApproximateTimeSpanComparer());
        }
    }
Ejemplo n.º 17
0
    private async ValueTask <ForwarderError> HandleUpgradedResponse(HttpContext context, HttpResponseMessage destinationResponse,
                                                                    ActivityCancellationTokenSource activityCancellationSource)
    {
        ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.ResponseUpgrade);

        // SocketHttpHandler and similar transports always provide an HttpContent object, even if it's empty.
        // Note as of 5.0 HttpResponse.Content never returns null.
        // https://github.com/dotnet/runtime/blame/8fc68f626a11d646109a758cb0fc70a0aa7826f1/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs#L46
        if (destinationResponse.Content == null)
        {
            throw new InvalidOperationException("A response content is required for upgrades.");
        }

        // :: Step 7-A-1: Upgrade the client channel. This will also send response headers.
        var upgradeFeature = context.Features.Get <IHttpUpgradeFeature>();

        if (upgradeFeature == null)
        {
            var ex = new InvalidOperationException("Invalid 101 response when upgrades aren't supported.");
            destinationResponse.Dispose();
            context.Response.StatusCode = StatusCodes.Status502BadGateway;
            ReportProxyError(context, ForwarderError.UpgradeResponseDestination, ex);
            return(ForwarderError.UpgradeResponseDestination);
        }

        RestoreUpgradeHeaders(context, destinationResponse);

        Stream upgradeResult;

        try
        {
            upgradeResult = await upgradeFeature.UpgradeAsync();
        }
        catch (Exception ex)
        {
            destinationResponse.Dispose();
            ReportProxyError(context, ForwarderError.UpgradeResponseClient, ex);
            return(ForwarderError.UpgradeResponseClient);
        }
        using var clientStream = upgradeResult;

        // :: Step 7-A-2: Copy duplex streams
        using var destinationStream = await destinationResponse.Content.ReadAsStreamAsync();

        var requestTask  = StreamCopier.CopyAsync(isRequest: true, clientStream, destinationStream, StreamCopier.UnknownLength, _clock, activityCancellationSource, activityCancellationSource.Token).AsTask();
        var responseTask = StreamCopier.CopyAsync(isRequest: false, destinationStream, clientStream, StreamCopier.UnknownLength, _clock, activityCancellationSource, activityCancellationSource.Token).AsTask();

        // Make sure we report the first failure.
        var firstTask = await Task.WhenAny(requestTask, responseTask);

        var requestFinishedFirst = firstTask == requestTask;
        var secondTask           = requestFinishedFirst ? responseTask : requestTask;

        ForwarderError error;

        var(firstResult, firstException) = await firstTask;
        if (firstResult != StreamCopyResult.Success)
        {
            error = ReportResult(context, requestFinishedFirst, firstResult, firstException);
            // Cancel the other direction
            activityCancellationSource.Cancel();
            // Wait for this to finish before exiting so the resources get cleaned up properly.
            await secondTask;
        }
        else
        {
            var(secondResult, secondException) = await secondTask;
            if (secondResult != StreamCopyResult.Success)
            {
                error = ReportResult(context, !requestFinishedFirst, secondResult, secondException !);
            }
            else
            {
                error = ForwarderError.None;
            }
        }

        return(error);

        ForwarderError ReportResult(HttpContext context, bool reqeuest, StreamCopyResult result, Exception exception)
        {
            var error = result switch
            {
                StreamCopyResult.InputError => reqeuest ? ForwarderError.UpgradeRequestClient : ForwarderError.UpgradeResponseDestination,
                StreamCopyResult.OutputError => reqeuest ? ForwarderError.UpgradeRequestDestination : ForwarderError.UpgradeResponseClient,
                StreamCopyResult.Canceled => reqeuest ? ForwarderError.UpgradeRequestCanceled : ForwarderError.UpgradeResponseCanceled,
                _ => throw new NotImplementedException(result.ToString()),
            };

            ReportProxyError(context, error, exception);
            return(error);
        }
    }
Ejemplo n.º 18
0
    private StreamCopyHttpContent?SetupRequestBodyCopy(HttpRequest request, bool isStreamingRequest, ActivityCancellationTokenSource activityToken)
    {
        // If we generate an HttpContent without a Content-Length then for HTTP/1.1 HttpClient will add a Transfer-Encoding: chunked header
        // even if it's a GET request. Some servers reject requests containing a Transfer-Encoding header if they're not expecting a body.
        // Try to be as specific as possible about the client's intent to send a body. The one thing we don't want to do is to start
        // reading the body early because that has side-effects like 100-continue.
        var hasBody       = true;
        var contentLength = request.Headers.ContentLength;
        var method        = request.Method;

#if NET
        var canHaveBodyFeature = request.HttpContext.Features.Get <IHttpRequestBodyDetectionFeature>();
        if (canHaveBodyFeature != null)
        {
            // 5.0 servers provide a definitive answer for us.
            hasBody = canHaveBodyFeature.CanHaveBody;
        }
        else
#endif
        // https://tools.ietf.org/html/rfc7230#section-3.3.3
        // All HTTP/1.1 requests should have Transfer-Encoding or Content-Length.
        // Http.Sys/IIS will even add a Transfer-Encoding header to HTTP/2 requests with bodies for back-compat.
        // HTTP/1.0 Connection: close bodies are only allowed on responses, not requests.
        // https://tools.ietf.org/html/rfc1945#section-7.2.2
        //
        // Transfer-Encoding overrides Content-Length per spec
        if (request.Headers.TryGetValue(HeaderNames.TransferEncoding, out var transferEncoding) &&
            transferEncoding.Count == 1 &&
            string.Equals("chunked", transferEncoding.ToString(), StringComparison.OrdinalIgnoreCase))
        {
            hasBody = true;
        }
        else if (contentLength.HasValue)
        {
            hasBody = contentLength > 0;
        }
        // Kestrel HTTP/2: There are no required headers that indicate if there is a request body so we need to sniff other fields.
        else if (!ProtocolHelper.IsHttp2OrGreater(request.Protocol))
        {
            hasBody = false;
        }
        // https://tools.ietf.org/html/rfc7231#section-4.3.1
        // A payload within a GET/HEAD/DELETE/CONNECT request message has no defined semantics; sending a payload body on a
        // GET/HEAD/DELETE/CONNECT request might cause some existing implementations to reject the request.
        // https://tools.ietf.org/html/rfc7231#section-4.3.8
        // A client MUST NOT send a message body in a TRACE request.
        else if (HttpMethods.IsGet(method) ||
                 HttpMethods.IsHead(method) ||
                 HttpMethods.IsDelete(method) ||
                 HttpMethods.IsConnect(method) ||
                 HttpMethods.IsTrace(method))
        {
            hasBody = false;
        }
        // else hasBody defaults to true

        if (hasBody)
        {
            if (isStreamingRequest)
            {
                DisableMinRequestBodyDataRateAndMaxRequestBodySize(request.HttpContext);
            }

            // Note on `autoFlushHttpClientOutgoingStream: isStreamingRequest`:
            // The.NET Core HttpClient stack keeps its own buffers on top of the underlying outgoing connection socket.
            // We flush those buffers down to the socket on every write when this is set,
            // but it does NOT result in calls to flush on the underlying socket.
            // This is necessary because we proxy http2 transparently,
            // and we are deliberately unaware of packet structure used e.g. in gRPC duplex channels.
            // Because the sockets aren't flushed, the perf impact of this choice is expected to be small.
            // Future: It may be wise to set this to true for *all* http2 incoming requests,
            // but for now, out of an abundance of caution, we only do it for requests that look like gRPC.
            return(new StreamCopyHttpContent(
                       source: request.Body,
                       autoFlushHttpClientOutgoingStream: isStreamingRequest,
                       clock: _clock,
                       activityToken));
        }

        return(null);
    }
Ejemplo n.º 19
0
    private async ValueTask <(HttpRequestMessage, StreamCopyHttpContent?)> CreateRequestMessageAsync(HttpContext context, string destinationPrefix,
                                                                                                     HttpTransformer transformer, ForwarderRequestConfig?requestConfig, bool isStreamingRequest, ActivityCancellationTokenSource activityToken)
    {
        // "http://a".Length = 8
        if (destinationPrefix == null || destinationPrefix.Length < 8)
        {
            throw new ArgumentException("Invalid destination prefix.", nameof(destinationPrefix));
        }

        var destinationRequest = new HttpRequestMessage();

        destinationRequest.Method = RequestUtilities.GetHttpMethod(context.Request.Method);

        var upgradeFeature   = context.Features.Get <IHttpUpgradeFeature>();
        var upgradeHeader    = context.Request.Headers[HeaderNames.Upgrade].ToString();
        var isUpgradeRequest = (upgradeFeature?.IsUpgradableRequest ?? false)
                               // Mitigate https://github.com/microsoft/reverse-proxy/issues/255, IIS considers all requests upgradeable.
                               && (string.Equals("WebSocket", upgradeHeader, StringComparison.OrdinalIgnoreCase)
                               // https://github.com/microsoft/reverse-proxy/issues/467 for kubernetes APIs
                                   || upgradeHeader.StartsWith("SPDY/", StringComparison.OrdinalIgnoreCase));

        // Default to HTTP/1.1 for proxying upgradeable requests. This is already the default as of .NET Core 3.1
        // Otherwise request what's set in proxyOptions (e.g. default HTTP/2) and let HttpClient negotiate the protocol
        // based on VersionPolicy (for .NET 5 and higher). For example, downgrading to HTTP/1.1 if it cannot establish HTTP/2 with the target.
        // This is done without extra round-trips thanks to ALPN. We can detect a downgrade after calling HttpClient.SendAsync
        // (see Step 3 below). TBD how this will change when HTTP/3 is supported.
        destinationRequest.Version = isUpgradeRequest ? ProtocolHelper.Http11Version : (requestConfig?.Version ?? DefaultVersion);
#if NET
        destinationRequest.VersionPolicy = isUpgradeRequest ? HttpVersionPolicy.RequestVersionOrLower : (requestConfig?.VersionPolicy ?? DefaultVersionPolicy);
#endif

        // :: Step 2: Setup copy of request body (background) Client --► Proxy --► Destination
        // Note that we must do this before step (3) because step (3) may also add headers to the HttpContent that we set up here.
        var requestContent = SetupRequestBodyCopy(context.Request, isStreamingRequest, activityToken);
        destinationRequest.Content = requestContent;

        // :: Step 3: Copy request headers Client --► Proxy --► Destination
        await transformer.TransformRequestAsync(context, destinationRequest, destinationPrefix);

        if (isUpgradeRequest)
        {
            RestoreUpgradeHeaders(context, destinationRequest);
        }

        // Allow someone to custom build the request uri, otherwise provide a default for them.
        var request = context.Request;
        destinationRequest.RequestUri ??= RequestUtilities.MakeDestinationAddress(destinationPrefix, request.Path, request.QueryString);

        Log.Proxying(_logger, destinationRequest, isStreamingRequest);

        if (requestConfig?.AllowResponseBuffering != true)
        {
            context.Features.Get <IHttpResponseBodyFeature>()?.DisableBuffering();
        }

        // TODO: What if they replace the HttpContent object? That would mess with our tracking and error handling.
        return(destinationRequest, requestContent);
    }
Ejemplo n.º 20
0
    public static ValueTask <(StreamCopyResult, Exception?)> CopyAsync(bool isRequest, Stream input, Stream output, long promisedContentLength, IClock clock, ActivityCancellationTokenSource activityToken, CancellationToken cancellation)
    {
        Debug.Assert(input is not null);
        Debug.Assert(output is not null);
        Debug.Assert(clock is not null);
        Debug.Assert(activityToken is not null);

        // Avoid capturing 'isRequest' and 'clock' in the state machine when telemetry is disabled
        var telemetry = ForwarderTelemetry.Log.IsEnabled(EventLevel.Informational, EventKeywords.All)
            ? new StreamCopierTelemetry(isRequest, clock)
            : null;

        return(CopyAsync(input, output, promisedContentLength, telemetry, activityToken, cancellation));
    }
Ejemplo n.º 21
0
    private static async ValueTask <(StreamCopyResult, Exception?)> CopyAsync(Stream input, Stream output, long promisedContentLength, StreamCopierTelemetry?telemetry, ActivityCancellationTokenSource activityToken, CancellationToken cancellation)
    {
        var buffer = ArrayPool <byte> .Shared.Rent(DefaultBufferSize);

        var  read          = 0;
        long contentLength = 0;

        try
        {
            while (true)
            {
                read = 0;

                // Issue a zero-byte read to the input stream to defer buffer allocation until data is available.
                // Note that if the underlying stream does not supporting blocking on zero byte reads, then this will
                // complete immediately and won't save any memory, but will still function correctly.
                var zeroByteReadTask = input.ReadAsync(Memory <byte> .Empty, cancellation);
                if (zeroByteReadTask.IsCompletedSuccessfully)
                {
                    // Consume the ValueTask's result in case it is backed by an IValueTaskSource
                    _ = zeroByteReadTask.Result;
                }
                else
                {
                    // Take care not to return the same buffer to the pool twice in case zeroByteReadTask throws
                    var bufferToReturn = buffer;
                    buffer = null;
                    ArrayPool <byte> .Shared.Return(bufferToReturn);

                    await zeroByteReadTask;

                    buffer = ArrayPool <byte> .Shared.Rent(DefaultBufferSize);
                }

                read = await input.ReadAsync(buffer.AsMemory(), cancellation);

                contentLength += read;
                // Normally this is enforced by the server, but it could get out of sync if something in the proxy modified the body.
                if (promisedContentLength != UnknownLength && contentLength > promisedContentLength)
                {
                    return(StreamCopyResult.InputError, new InvalidOperationException("More bytes received than the specified Content-Length."));
                }

                telemetry?.AfterRead(contentLength);

                // Success, reset the activity monitor.
                activityToken.ResetTimeout();

                // End of the source stream.
                if (read == 0)
                {
                    if (promisedContentLength == UnknownLength || contentLength == promisedContentLength)
                    {
                        return(StreamCopyResult.Success, null);
                    }
                    else
                    {
                        // This can happen if something in the proxy consumes or modifies part or all of the request body before proxying.
                        return(StreamCopyResult.InputError,
                               new InvalidOperationException($"Sent {contentLength} request content bytes, but Content-Length promised {promisedContentLength}."));
                    }
                }

                await output.WriteAsync(buffer.AsMemory(0, read), cancellation);

                telemetry?.AfterWrite();

                // Success, reset the activity monitor.
                activityToken.ResetTimeout();
            }
        }
        catch (Exception ex)
        {
            if (read == 0)
            {
                telemetry?.AfterRead(contentLength);
            }
            else
            {
                telemetry?.AfterWrite();
            }

            var result = ex is OperationCanceledException ? StreamCopyResult.Canceled :
                         (read == 0 ? StreamCopyResult.InputError : StreamCopyResult.OutputError);

            return(result, ex);
        }
        finally
        {
            if (buffer is not null)
            {
                ArrayPool <byte> .Shared.Return(buffer);
            }

            telemetry?.Stop();
        }
    }