// 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.Buffers; using System.Collections.Generic; using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; using Moq; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { public class HttpConnectionTests : IDisposable { private readonly MemoryPool _memoryPool; private readonly HttpConnectionContext _httpConnectionContext; private readonly HttpConnection _httpConnection; public HttpConnectionTests() { _memoryPool = KestrelMemoryPool.Create(); var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); var connectionFeatures = new FeatureCollection(); connectionFeatures.Set(Mock.Of()); connectionFeatures.Set(Mock.Of()); _httpConnectionContext = new HttpConnectionContext { ConnectionId = "0123456789", ConnectionContext = Mock.Of(), ConnectionAdapters = new List(), ConnectionFeatures = connectionFeatures, MemoryPool = _memoryPool, Transport = pair.Transport, ServiceContext = new TestServiceContext { SystemClock = new SystemClock() } }; _httpConnection = new HttpConnection(_httpConnectionContext); } public void Dispose() { _memoryPool.Dispose(); } [Fact] public void DoesNotTimeOutWhenDebuggerIsAttached() { var mockDebugger = new Mock(); mockDebugger.SetupGet(g => g.IsAttached).Returns(true); _httpConnection.Debugger = mockDebugger.Object; _httpConnection.Initialize(_httpConnectionContext.Transport); var now = DateTimeOffset.Now; _httpConnection.Tick(now); _httpConnection.SetTimeout(1, TimeoutAction.SendTimeoutResponse); _httpConnection.Tick(now.AddTicks(2).Add(Heartbeat.Interval)); Assert.False(_httpConnection.RequestTimedOut); } [Fact] public void DoesNotTimeOutWhenRequestBodyDoesNotSatisfyMinimumDataRateButDebuggerIsAttached() { var mockDebugger = new Mock(); mockDebugger.SetupGet(g => g.IsAttached).Returns(true); _httpConnection.Debugger = mockDebugger.Object; var bytesPerSecond = 100; var mockLogger = new Mock(); mockLogger.Setup(l => l.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny())).Throws(new InvalidOperationException("Should not log")); TickBodyWithMinimumDataRate(mockLogger.Object, bytesPerSecond); Assert.False(_httpConnection.RequestTimedOut); } [Fact] public void TimesOutWhenRequestBodyDoesNotSatisfyMinimumDataRate() { var bytesPerSecond = 100; var mockLogger = new Mock(); TickBodyWithMinimumDataRate(mockLogger.Object, bytesPerSecond); // Timed out Assert.True(_httpConnection.RequestTimedOut); mockLogger.Verify(logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), bytesPerSecond), Times.Once); } private void TickBodyWithMinimumDataRate(IKestrelTrace logger, int bytesPerSecond) { var gracePeriod = TimeSpan.FromSeconds(5); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate = new MinDataRate(bytesPerSecond: bytesPerSecond, gracePeriod: gracePeriod); _httpConnectionContext.ServiceContext.Log = logger; _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); // Initialize timestamp var now = DateTimeOffset.UtcNow; _httpConnection.Tick(now); _httpConnection.StartTimingReads(); // Tick after grace period w/ low data rate now += gracePeriod + TimeSpan.FromSeconds(1); _httpConnection.BytesRead(1); _httpConnection.Tick(now); } [Fact] public void RequestBodyMinimumDataRateNotEnforcedDuringGracePeriod() { var bytesPerSecond = 100; var gracePeriod = TimeSpan.FromSeconds(2); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate = new MinDataRate(bytesPerSecond: bytesPerSecond, gracePeriod: gracePeriod); var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); // Initialize timestamp var now = DateTimeOffset.UtcNow; _httpConnection.Tick(now); _httpConnection.StartTimingReads(); // Tick during grace period w/ low data rate now += TimeSpan.FromSeconds(1); _httpConnection.BytesRead(10); _httpConnection.Tick(now); // Not timed out Assert.False(_httpConnection.RequestTimedOut); mockLogger.Verify(logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), bytesPerSecond), Times.Never); // Tick after grace period w/ low data rate now += TimeSpan.FromSeconds(2); _httpConnection.BytesRead(10); _httpConnection.Tick(now); // Timed out Assert.True(_httpConnection.RequestTimedOut); mockLogger.Verify(logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), bytesPerSecond), Times.Once); } [Fact] public void RequestBodyDataRateIsAveragedOverTimeSpentReadingRequestBody() { var bytesPerSecond = 100; var gracePeriod = TimeSpan.FromSeconds(2); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate = new MinDataRate(bytesPerSecond: bytesPerSecond, gracePeriod: gracePeriod); var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); // Initialize timestamp var now = DateTimeOffset.UtcNow; _httpConnection.Tick(now); _httpConnection.StartTimingReads(); // Set base data rate to 200 bytes/second now += gracePeriod; _httpConnection.BytesRead(400); _httpConnection.Tick(now); // Data rate: 200 bytes/second now += TimeSpan.FromSeconds(1); _httpConnection.BytesRead(200); _httpConnection.Tick(now); // Not timed out Assert.False(_httpConnection.RequestTimedOut); mockLogger.Verify(logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), bytesPerSecond), Times.Never); // Data rate: 150 bytes/second now += TimeSpan.FromSeconds(1); _httpConnection.BytesRead(0); _httpConnection.Tick(now); // Not timed out Assert.False(_httpConnection.RequestTimedOut); mockLogger.Verify(logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), bytesPerSecond), Times.Never); // Data rate: 120 bytes/second now += TimeSpan.FromSeconds(1); _httpConnection.BytesRead(0); _httpConnection.Tick(now); // Not timed out Assert.False(_httpConnection.RequestTimedOut); mockLogger.Verify(logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), bytesPerSecond), Times.Never); // Data rate: 100 bytes/second now += TimeSpan.FromSeconds(1); _httpConnection.BytesRead(0); _httpConnection.Tick(now); // Not timed out Assert.False(_httpConnection.RequestTimedOut); mockLogger.Verify(logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), bytesPerSecond), Times.Never); // Data rate: ~85 bytes/second now += TimeSpan.FromSeconds(1); _httpConnection.BytesRead(0); _httpConnection.Tick(now); // Timed out Assert.True(_httpConnection.RequestTimedOut); mockLogger.Verify(logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), bytesPerSecond), Times.Once); } [Fact] public void RequestBodyDataRateNotComputedOnPausedTime() { var systemClock = new MockSystemClock(); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); _httpConnectionContext.ServiceContext.SystemClock = systemClock; var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); // Initialize timestamp _httpConnection.Tick(systemClock.UtcNow); _httpConnection.StartTimingReads(); // Tick at 3s, expected counted time is 3s, expected data rate is 200 bytes/second systemClock.UtcNow += TimeSpan.FromSeconds(3); _httpConnection.BytesRead(600); _httpConnection.Tick(systemClock.UtcNow); // Pause at 3.5s systemClock.UtcNow += TimeSpan.FromSeconds(0.5); _httpConnection.PauseTimingReads(); // Tick at 4s, expected counted time is 4s (first tick after pause goes through), expected data rate is 150 bytes/second systemClock.UtcNow += TimeSpan.FromSeconds(0.5); _httpConnection.Tick(systemClock.UtcNow); // Tick at 6s, expected counted time is 4s, expected data rate is 150 bytes/second systemClock.UtcNow += TimeSpan.FromSeconds(2); _httpConnection.Tick(systemClock.UtcNow); // Not timed out Assert.False(_httpConnection.RequestTimedOut); mockLogger.Verify( logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); // Resume at 6.5s systemClock.UtcNow += TimeSpan.FromSeconds(0.5); _httpConnection.ResumeTimingReads(); // Tick at 9s, expected counted time is 6s, expected data rate is 100 bytes/second systemClock.UtcNow += TimeSpan.FromSeconds(1.5); _httpConnection.Tick(systemClock.UtcNow); // Not timed out Assert.False(_httpConnection.RequestTimedOut); mockLogger.Verify( logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); // Tick at 10s, expected counted time is 7s, expected data rate drops below 100 bytes/second systemClock.UtcNow += TimeSpan.FromSeconds(1); _httpConnection.Tick(systemClock.UtcNow); // Timed out Assert.True(_httpConnection.RequestTimedOut); mockLogger.Verify( logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] public void ReadTimingNotPausedWhenResumeCalledBeforeNextTick() { var systemClock = new MockSystemClock(); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); _httpConnectionContext.ServiceContext.SystemClock = systemClock; var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); // Initialize timestamp _httpConnection.Tick(systemClock.UtcNow); _httpConnection.StartTimingReads(); // Tick at 2s, expected counted time is 2s, expected data rate is 100 bytes/second systemClock.UtcNow += TimeSpan.FromSeconds(2); _httpConnection.BytesRead(200); _httpConnection.Tick(systemClock.UtcNow); // Not timed out Assert.False(_httpConnection.RequestTimedOut); mockLogger.Verify( logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); // Pause at 2.25s systemClock.UtcNow += TimeSpan.FromSeconds(0.25); _httpConnection.PauseTimingReads(); // Resume at 2.5s systemClock.UtcNow += TimeSpan.FromSeconds(0.25); _httpConnection.ResumeTimingReads(); // Tick at 3s, expected counted time is 3s, expected data rate is 100 bytes/second systemClock.UtcNow += TimeSpan.FromSeconds(0.5); _httpConnection.BytesRead(100); _httpConnection.Tick(systemClock.UtcNow); // Not timed out Assert.False(_httpConnection.RequestTimedOut); mockLogger.Verify( logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); // Tick at 4s, expected counted time is 4s, expected data rate drops below 100 bytes/second systemClock.UtcNow += TimeSpan.FromSeconds(1); _httpConnection.Tick(systemClock.UtcNow); // Timed out Assert.True(_httpConnection.RequestTimedOut); mockLogger.Verify( logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] public void ReadTimingNotEnforcedWhenTimeoutIsSet() { var systemClock = new MockSystemClock(); var timeout = TimeSpan.FromSeconds(5); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); _httpConnectionContext.ServiceContext.SystemClock = systemClock; var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); var startTime = systemClock.UtcNow; // Initialize timestamp _httpConnection.Tick(startTime); _httpConnection.StartTimingReads(); _httpConnection.SetTimeout(timeout.Ticks, TimeoutAction.StopProcessingNextRequest); // Tick beyond grace period with low data rate systemClock.UtcNow += TimeSpan.FromSeconds(3); _httpConnection.BytesRead(1); _httpConnection.Tick(systemClock.UtcNow); // Not timed out Assert.False(_httpConnection.RequestTimedOut); // Tick just past timeout period, adjusted by Heartbeat.Interval systemClock.UtcNow = startTime + timeout + Heartbeat.Interval + TimeSpan.FromTicks(1); _httpConnection.Tick(systemClock.UtcNow); // Timed out Assert.True(_httpConnection.RequestTimedOut); } [Fact] public async Task WriteTimingAbortsConnectionWhenWriteDoesNotCompleteWithMinimumDataRate() { var systemClock = new MockSystemClock(); var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); _httpConnectionContext.ServiceContext.SystemClock = systemClock; var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { aborted.SetResult(null); }); // Initialize timestamp _httpConnection.Tick(systemClock.UtcNow); // Should complete within 4 seconds, but the timeout is adjusted by adding Heartbeat.Interval _httpConnection.StartTimingWrite(400); // Tick just past 4s plus Heartbeat.Interval systemClock.UtcNow += TimeSpan.FromSeconds(4) + Heartbeat.Interval + TimeSpan.FromTicks(1); _httpConnection.Tick(systemClock.UtcNow); Assert.True(_httpConnection.RequestTimedOut); await aborted.Task.DefaultTimeout(); } [Fact] public async Task WriteTimingAbortsConnectionWhenSmallWriteDoesNotCompleteWithinGracePeriod() { var systemClock = new MockSystemClock(); var minResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5)); var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = minResponseDataRate; _httpConnectionContext.ServiceContext.SystemClock = systemClock; var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { aborted.SetResult(null); }); // Initialize timestamp var startTime = systemClock.UtcNow; _httpConnection.Tick(startTime); // Should complete within 1 second, but the timeout is adjusted by adding Heartbeat.Interval _httpConnection.StartTimingWrite(100); // Tick just past 1s plus Heartbeat.Interval systemClock.UtcNow += TimeSpan.FromSeconds(1) + Heartbeat.Interval + TimeSpan.FromTicks(1); _httpConnection.Tick(systemClock.UtcNow); // Still within grace period, not timed out Assert.False(_httpConnection.RequestTimedOut); // Tick just past grace period (adjusted by Heartbeat.Interval) systemClock.UtcNow = startTime + minResponseDataRate.GracePeriod + Heartbeat.Interval + TimeSpan.FromTicks(1); _httpConnection.Tick(systemClock.UtcNow); Assert.True(_httpConnection.RequestTimedOut); await aborted.Task.DefaultTimeout(); } [Fact] public async Task WriteTimingTimeoutPushedOnConcurrentWrite() { var systemClock = new MockSystemClock(); var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); _httpConnectionContext.ServiceContext.SystemClock = systemClock; var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { aborted.SetResult(null); }); // Initialize timestamp _httpConnection.Tick(systemClock.UtcNow); // Should complete within 5 seconds, but the timeout is adjusted by adding Heartbeat.Interval _httpConnection.StartTimingWrite(500); // Start a concurrent write after 3 seconds, which should complete within 3 seconds (adjusted by Heartbeat.Interval) _httpConnection.StartTimingWrite(300); // Tick just past 5s plus Heartbeat.Interval, when the first write should have completed systemClock.UtcNow += TimeSpan.FromSeconds(5) + Heartbeat.Interval + TimeSpan.FromTicks(1); _httpConnection.Tick(systemClock.UtcNow); // Not timed out because the timeout was pushed by the second write Assert.False(_httpConnection.RequestTimedOut); // Complete the first write, this should have no effect on the timeout _httpConnection.StopTimingWrite(); // Tick just past +3s, when the second write should have completed systemClock.UtcNow += TimeSpan.FromSeconds(3) + TimeSpan.FromTicks(1); _httpConnection.Tick(systemClock.UtcNow); Assert.True(_httpConnection.RequestTimedOut); await aborted.Task.DefaultTimeout(); } [Fact] public async Task WriteTimingAbortsConnectionWhenRepeadtedSmallWritesDoNotCompleteWithMinimumDataRate() { var systemClock = new MockSystemClock(); var minResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5)); var numWrites = 5; var writeSize = 100; var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = minResponseDataRate; _httpConnectionContext.ServiceContext.SystemClock = systemClock; var mockLogger = new Mock(); _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.Initialize(_httpConnectionContext.Transport); _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { aborted.SetResult(null); }); // Initialize timestamp var startTime = systemClock.UtcNow; _httpConnection.Tick(startTime); // 5 consecutive 100 byte writes. for (var i = 0; i < numWrites - 1; i++) { _httpConnection.StartTimingWrite(writeSize); _httpConnection.StopTimingWrite(); } // Stall the last write. _httpConnection.StartTimingWrite(writeSize); // Move the clock forward Heartbeat.Interval + MinDataRate.GracePeriod + 4 seconds. // The grace period should only be added for the first write. The subsequent 4 100 byte writes should add 1 second each to the timeout given the 100 byte/s min rate. systemClock.UtcNow += Heartbeat.Interval + minResponseDataRate.GracePeriod + TimeSpan.FromSeconds((numWrites - 1) * writeSize / minResponseDataRate.BytesPerSecond); _httpConnection.Tick(systemClock.UtcNow); Assert.False(_httpConnection.RequestTimedOut); // On more tick forward triggers the timeout. systemClock.UtcNow += TimeSpan.FromTicks(1); _httpConnection.Tick(systemClock.UtcNow); Assert.True(_httpConnection.RequestTimedOut); await aborted.Task.TimeoutAfter(TimeSpan.FromSeconds(10)); } } }