// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Net.Http.Headers; using Moq; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { public class Http2TimeoutTests : Http2TestBase { [Fact] public async Task Preamble_NotReceivedInitially_WithinKeepAliveTimeout_ClosesConnection() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; _timeoutControl.Initialize(mockSystemClock.UtcNow); CreateConnection(); _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(_noopApplication)); AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.KeepAlive), Times.Once); await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); _mockTimeoutHandler.VerifyNoOtherCalls(); } [Fact] public async Task HEADERS_NotReceivedInitially_WithinKeepAliveTimeout_ClosesConnection() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_noopApplication); AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.KeepAlive), Times.Once); await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); _mockTimeoutHandler.VerifyNoOtherCalls(); } [Fact] public async Task HEADERS_NotReceivedAfterFirstRequest_WithinKeepAliveTimeout_ClosesConnection() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_noopApplication); AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval); // keep-alive timeout set but not fired. _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // The KeepAlive timeout is set when the stream completes processing on a background thread, so we need to hook the // keep-alive set afterwards to make a reliable test. var setTimeoutTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _mockTimeoutControl.Setup(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive)).Callback((t, r) => { _timeoutControl.SetTimeout(t, r); setTimeoutTcs.SetResult(null); }); // Send continuation frame to verify intermediate request header timeout doesn't interfere with keep-alive timeout. await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM, _browserRequestHeaders); await SendEmptyContinuationFrameAsync(1, Http2ContinuationFrameFlags.END_HEADERS); _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.RequestHeaders), Times.Once); await ExpectAsync(Http2FrameType.HEADERS, withLength: 55, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, withLength: 0, withFlags: (byte)Http2DataFrameFlags.END_STREAM, withStreamId: 1); await setTimeoutTcs.Task.DefaultTimeout(); AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.KeepAlive), Times.Once); await WaitForConnectionStopAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); _mockTimeoutHandler.VerifyNoOtherCalls(); } [Fact] public async Task HEADERS_ReceivedWithoutAllCONTINUATIONs_WithinRequestHeadersTimeout_AbortsConnection() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits;; _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_noopApplication); await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM, _browserRequestHeaders); await SendEmptyContinuationFrameAsync(1, Http2ContinuationFrameFlags.NONE); AdvanceClock(limits.RequestHeadersTimeout + Heartbeat.Interval); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); await SendEmptyContinuationFrameAsync(1, Http2ContinuationFrameFlags.NONE); AdvanceClock(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.RequestHeaders), Times.Once); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, CoreStrings.BadRequest_RequestHeadersTimeout); _mockConnectionContext.Verify(c =>c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestHeadersTimeout)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } [Fact] public async Task ResponseDrain_SlowerThanMinimumDataRate_AbortsConnection() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_noopApplication); await SendGoAwayAsync(); await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); AdvanceClock(TimeSpan.FromSeconds(_bytesReceived / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); _mockConnectionContext.Verify(c => c.Abort(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); _mockConnectionContext.Verify(c =>c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } [Theory] [InlineData(Http2FrameType.DATA)] [InlineData(Http2FrameType.CONTINUATION, Skip = "https://github.com/aspnet/KestrelHttpServer/issues/3077")] public async Task AbortedStream_ResetsAndDrainsRequest_RefusesFramesAfterCooldownExpires(Http2FrameType finalFrameType) { var mockSystemClock = _serviceContext.MockSystemClock; var headers = new[] { new KeyValuePair(HeaderNames.Method, "POST"), new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; await InitializeConnectionAsync(_appAbort); await StartStreamAsync(1, headers, endStream: false); await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, "The connection was aborted by the application."); var cts = new CancellationTokenSource(); async Task AdvanceClockAndSendFrames() { if (finalFrameType == Http2FrameType.CONTINUATION) { await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM, new byte[0]); } // There's a race when the appfunc is exiting about how soon it unregisters the stream, so retry until success. while (!cts.Token.IsCancellationRequested) { // Just past the timeout mockSystemClock.UtcNow += Constants.RequestBodyDrainTimeout + TimeSpan.FromTicks(1); (_connection as IRequestProcessor).Tick(mockSystemClock.UtcNow); // Send an extra frame to make it fail switch (finalFrameType) { case Http2FrameType.DATA: await SendDataAsync(1, new byte[100], endStream: false); break; case Http2FrameType.CONTINUATION: await SendContinuationAsync(1, Http2ContinuationFrameFlags.NONE, new byte[0]); break; default: throw new NotImplementedException(finalFrameType.ToString()); } if (!cts.Token.IsCancellationRequested) { await Task.Delay(10); } } } var sendTask = AdvanceClockAndSendFrames(); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.STREAM_CLOSED, CoreStrings.FormatHttp2ErrorStreamClosed(finalFrameType, 1)); cts.Cancel(); await sendTask.DefaultTimeout(); } [Fact] public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnSmallWrite_AbortsConnectionAfterGracePeriod() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); // Disable response buffering so "socket" backpressure is observed immediately. limits.MaxResponseBufferSize = 0; _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_echoApplication); await StartStreamAsync(1, _browserRequestHeaders, endStream: false); await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); // Complete timing of the request body so we don't induce any unexpected request body rate timeouts. _timeoutControl.Tick(mockSystemClock.UtcNow); // Don't read data frame to induce "socket" backpressure. AdvanceClock(TimeSpan.FromSeconds((_bytesReceived + _helloWorldBytes.Length) / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); // The "hello, world" bytes are buffered from before the timeout, but not an END_STREAM data frame. await ExpectAsync(Http2FrameType.DATA, withLength: _helloWorldBytes.Length, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 1); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, null); _mockConnectionContext.Verify(c =>c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } [Fact] public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnLargeWrite_AbortsConnectionAfterRateTimeout() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); // Disable response buffering so "socket" backpressure is observed immediately. limits.MaxResponseBufferSize = 0; _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_echoApplication); await StartStreamAsync(1, _browserRequestHeaders, endStream: false); await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); // Complete timing of the request body so we don't induce any unexpected request body rate timeouts. _timeoutControl.Tick(mockSystemClock.UtcNow); var timeToWriteMaxData = TimeSpan.FromSeconds((_bytesReceived + _maxData.Length) / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5); // Don't read data frame to induce "socket" backpressure. AdvanceClock(timeToWriteMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); // The _maxData bytes are buffered from before the timeout, but not an END_STREAM data frame. await ExpectAsync(Http2FrameType.DATA, withLength: _maxData.Length, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 1); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, null); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } [Fact] public async Task DATA_Sent_TooSlowlyDueToFlowControlOnSmallWrite_AbortsConnectionAfterGracePeriod() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); // This only affects the stream windows. The connection-level window is always initialized at 64KiB. _clientSettings.InitialWindowSize = 6; _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_echoApplication); await StartStreamAsync(1, _browserRequestHeaders, endStream: false); await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, withLength: (int)_clientSettings.InitialWindowSize, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 1); // Complete timing of the request body so we don't induce any unexpected request body rate timeouts. _timeoutControl.Tick(mockSystemClock.UtcNow); // Don't send WINDOW_UPDATE to induce flow-control backpressure AdvanceClock(TimeSpan.FromSeconds(_bytesReceived / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, null); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } [Fact] public async Task DATA_Sent_TooSlowlyDueToOutputFlowControlOnLargeWrite_AbortsConnectionAfterRateTimeout() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); // This only affects the stream windows. The connection-level window is always initialized at 64KiB. _clientSettings.InitialWindowSize = (uint)_maxData.Length - 1; _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_echoApplication); await StartStreamAsync(1, _browserRequestHeaders, endStream: false); await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, withLength: (int)_clientSettings.InitialWindowSize, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 1); // Complete timing of the request body so we don't induce any unexpected request body rate timeouts. _timeoutControl.Tick(mockSystemClock.UtcNow); var timeToWriteMaxData = TimeSpan.FromSeconds(_bytesReceived / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5); // Don't send WINDOW_UPDATE to induce flow-control backpressure AdvanceClock(timeToWriteMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, null); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } [Fact] public async Task DATA_Sent_TooSlowlyDueToOutputFlowControlOnMultipleStreams_AbortsConnectionAfterAdditiveRateTimeout() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); // This only affects the stream windows. The connection-level window is always initialized at 64KiB. _clientSettings.InitialWindowSize = (uint)_maxData.Length - 1; _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_echoApplication); await StartStreamAsync(1, _browserRequestHeaders, endStream: false); await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, withLength: (int)_clientSettings.InitialWindowSize, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 1); await StartStreamAsync(3, _browserRequestHeaders, endStream: false); await SendDataAsync(3, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, withLength: (int)_clientSettings.InitialWindowSize, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 3); // Complete timing of the request bodies so we don't induce any unexpected request body rate timeouts. _timeoutControl.Tick(mockSystemClock.UtcNow); var timeToWriteMaxData = TimeSpan.FromSeconds(_bytesReceived / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5); // Don't send WINDOW_UPDATE to induce flow-control backpressure AdvanceClock(timeToWriteMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 3, Http2ErrorCode.INTERNAL_ERROR, null); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } [Fact] public async Task DATA_Received_TooSlowlyOnSmallRead_AbortsConnectionAfterGracePeriod() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_echoApplication); // _helloWorldBytes is 12 bytes, and 12 bytes / 240 bytes/sec = .05 secs which is far below the grace period. await StartStreamAsync(1, _browserRequestHeaders, endStream: false); await SendDataAsync(1, _helloWorldBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, withLength: _helloWorldBytes.Length, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 1); // Don't send any more data and advance just to and then past the grace period. AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, null); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } [Fact] public async Task DATA_Received_TooSlowlyOnLargeRead_AbortsConnectionAfterRateTimeout() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_echoApplication); // _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period. await StartStreamAsync(1, _browserRequestHeaders, endStream: false); await SendDataAsync(1, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, withLength: _maxData.Length, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 1); // Due to the imprecision of floating point math and the fact that TimeoutControl derives rate from elapsed // time for reads instead of vice versa like for writes, use a half-second instead of single-tick cushion. var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. AdvanceClock(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, null); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } [Fact] public async Task DATA_Received_TooSlowlyOnMultipleStreams_AbortsConnectionAfterAdditiveRateTimeout() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_echoApplication); // _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period. await StartStreamAsync(1, _browserRequestHeaders, endStream: false); await SendDataAsync(1, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, withLength: _maxData.Length, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 1); await StartStreamAsync(3, _browserRequestHeaders, endStream: false); await SendDataAsync(3, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, withLength: _maxData.Length, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 3); var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond); // Double the timeout for the second stream. timeToReadMaxData += timeToReadMaxData; // Due to the imprecision of floating point math and the fact that TimeoutControl derives rate from elapsed // time for reads instead of vice versa like for writes, use a half-second instead of single-tick cushion. timeToReadMaxData -= TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. AdvanceClock(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 3, Http2ErrorCode.INTERNAL_ERROR, null); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } [Fact] public async Task DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNonAdditiveRateTimeout() { var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(_echoApplication); // _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period. await StartStreamAsync(1, _browserRequestHeaders, endStream: false); await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, withLength: _maxData.Length, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, withLength: 0, withFlags: (byte)Http2DataFrameFlags.END_STREAM, withStreamId: 1); await StartStreamAsync(3, _browserRequestHeaders, endStream: false); await SendDataAsync(3, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, withLength: _maxData.Length, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 3); // Due to the imprecision of floating point math and the fact that TimeoutControl derives rate from elapsed // time for reads instead of vice versa like for writes, use a half-second instead of single-tick cushion. var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. AdvanceClock(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 3, Http2ErrorCode.INTERNAL_ERROR, null); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } [Fact] public async Task DATA_Received_SlowlyDueToConnectionFlowControl_DoesNotAbortConnection() { var initialConnectionWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize; var framesConnectionInWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize; var backpressureTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var mockSystemClock = _serviceContext.MockSystemClock; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); _timeoutControl.Initialize(mockSystemClock.UtcNow); await InitializeConnectionAsync(async context => { var streamId = context.Features.Get().StreamId; if (streamId == 1) { await backpressureTcs.Task; } else { await _echoApplication(context); } }); await StartStreamAsync(1, _browserRequestHeaders, endStream: false); for (var i = 0; i < framesConnectionInWindow / 2; i++) { await SendDataAsync(1, _maxData, endStream: false); } await SendDataAsync(1, _maxData, endStream: true); await StartStreamAsync(3, _browserRequestHeaders, endStream: false); await SendDataAsync(3, _helloWorldBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, withLength: _helloWorldBytes.Length, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 3); // No matter how much time elapses there is no read timeout because the connection window is too small. AdvanceClock(TimeSpan.FromDays(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Opening the connection window starts the read rate timeout enforcement after that point. backpressureTcs.SetResult(null); await ExpectAsync(Http2FrameType.HEADERS, withLength: 55, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, withLength: 0, withFlags: (byte)Http2DataFrameFlags.END_STREAM, withStreamId: 1); await ExpectAsync(Http2FrameType.WINDOW_UPDATE, withLength: 4, withFlags: (byte)Http2DataFrameFlags.NONE, withStreamId: 0); AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); AdvanceClock(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 3, Http2ErrorCode.INTERNAL_ERROR, null); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); } } }