parent
31f60d75d3
commit
1f2e704dd3
|
|
@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||||
var bufferSize = serviceContext.ServerOptions.Limits.MaxResponseBufferSize;
|
var bufferSize = serviceContext.ServerOptions.Limits.MaxResponseBufferSize;
|
||||||
if (bufferSize == 0)
|
if (bufferSize == 0)
|
||||||
{
|
{
|
||||||
// 0 = no buffering so we need to configure the pipe so the the writer waits on the reader directly
|
// 0 = no buffering so we need to configure the pipe so the writer waits on the reader directly
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
private readonly OutputFlowControl _connectionOutputFlowControl;
|
private readonly OutputFlowControl _connectionOutputFlowControl;
|
||||||
private readonly string _connectionId;
|
private readonly string _connectionId;
|
||||||
private readonly IKestrelTrace _log;
|
private readonly IKestrelTrace _log;
|
||||||
|
private readonly ITimeoutControl _timeoutControl;
|
||||||
private readonly TimingPipeFlusher _flusher;
|
private readonly TimingPipeFlusher _flusher;
|
||||||
|
|
||||||
private bool _completed;
|
private bool _completed;
|
||||||
|
|
@ -54,6 +55,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
_connectionOutputFlowControl = connectionOutputFlowControl;
|
_connectionOutputFlowControl = connectionOutputFlowControl;
|
||||||
_connectionId = connectionId;
|
_connectionId = connectionId;
|
||||||
_log = log;
|
_log = log;
|
||||||
|
_timeoutControl = timeoutControl;
|
||||||
_flusher = new TimingPipeFlusher(_outputWriter, timeoutControl);
|
_flusher = new TimingPipeFlusher(_outputWriter, timeoutControl);
|
||||||
_outgoingFrame = new Http2Frame();
|
_outgoingFrame = new Http2Frame();
|
||||||
_headerEncodingBuffer = new byte[_maxFrameSize];
|
_headerEncodingBuffer = new byte[_maxFrameSize];
|
||||||
|
|
@ -226,7 +228,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, bool endStream)
|
public Task WriteDataAsync(int streamId, StreamOutputFlowControl flowControl, MinDataRate minRate, ReadOnlySequence<byte> data, bool endStream)
|
||||||
{
|
{
|
||||||
// The Length property of a ReadOnlySequence can be expensive, so we cache the value.
|
// The Length property of a ReadOnlySequence can be expensive, so we cache the value.
|
||||||
var dataLength = data.Length;
|
var dataLength = data.Length;
|
||||||
|
|
@ -242,12 +244,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
// https://httpwg.org/specs/rfc7540.html#rfc.section.6.9.1
|
// https://httpwg.org/specs/rfc7540.html#rfc.section.6.9.1
|
||||||
if (dataLength != 0 && dataLength > flowControl.Available)
|
if (dataLength != 0 && dataLength > flowControl.Available)
|
||||||
{
|
{
|
||||||
return WriteDataAsyncAwaited(streamId, flowControl, data, dataLength, endStream);
|
return WriteDataAsyncAwaited(streamId, minRate, flowControl, data, dataLength, endStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This cast is safe since if dataLength would overflow an int, it's guaranteed to be greater than the available flow control window.
|
// This cast is safe since if dataLength would overflow an int, it's guaranteed to be greater than the available flow control window.
|
||||||
flowControl.Advance((int)dataLength);
|
flowControl.Advance((int)dataLength);
|
||||||
return WriteDataUnsynchronizedAsync(streamId, data, endStream);
|
return WriteDataUnsynchronizedAsync(streamId, minRate, data, dataLength, endStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,7 +262,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
| Padding (*) ...
|
| Padding (*) ...
|
||||||
+---------------------------------------------------------------+
|
+---------------------------------------------------------------+
|
||||||
*/
|
*/
|
||||||
private Task WriteDataUnsynchronizedAsync(int streamId, ReadOnlySequence<byte> data, bool endStream)
|
private Task WriteDataUnsynchronizedAsync(int streamId, MinDataRate minRate, ReadOnlySequence<byte> data, long dataLength, bool endStream)
|
||||||
{
|
{
|
||||||
// Note padding is not implemented
|
// Note padding is not implemented
|
||||||
_outgoingFrame.PrepareData(streamId);
|
_outgoingFrame.PrepareData(streamId);
|
||||||
|
|
@ -300,15 +302,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|
|
||||||
// Plus padding
|
// Plus padding
|
||||||
|
|
||||||
return _flusher.FlushAsync();
|
return _flusher.FlushAsync(minRate, dataLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task WriteDataAsyncAwaited(int streamId, StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, long dataLength, bool endStream)
|
private async Task WriteDataAsyncAwaited(int streamId, MinDataRate minRate, StreamOutputFlowControl flowControl, ReadOnlySequence<byte> data, long dataLength, bool endStream)
|
||||||
{
|
{
|
||||||
while (dataLength > 0)
|
while (dataLength > 0)
|
||||||
{
|
{
|
||||||
OutputFlowControlAwaitable availabilityAwaitable;
|
OutputFlowControlAwaitable availabilityAwaitable;
|
||||||
var writeTask = Task.CompletedTask;
|
var writeTask = Task.CompletedTask;
|
||||||
|
int actual;
|
||||||
|
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
{
|
{
|
||||||
|
|
@ -317,24 +320,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var actual = flowControl.AdvanceUpToAndWait(dataLength, out availabilityAwaitable);
|
actual = flowControl.AdvanceUpToAndWait(dataLength, out availabilityAwaitable);
|
||||||
|
|
||||||
if (actual > 0)
|
if (actual > 0)
|
||||||
{
|
{
|
||||||
|
// Don't pass minRate through to the inner WriteData calls.
|
||||||
|
// We measure this ourselves below so we account for flow control in addition to socket backpressure.
|
||||||
if (actual < dataLength)
|
if (actual < dataLength)
|
||||||
{
|
{
|
||||||
writeTask = WriteDataUnsynchronizedAsync(streamId, data.Slice(0, actual), endStream: false);
|
writeTask = WriteDataUnsynchronizedAsync(streamId, null, data.Slice(0, actual), actual, endStream: false);
|
||||||
data = data.Slice(actual);
|
data = data.Slice(actual);
|
||||||
dataLength -= actual;
|
dataLength -= actual;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
writeTask = WriteDataUnsynchronizedAsync(streamId, data, endStream);
|
writeTask = WriteDataUnsynchronizedAsync(streamId, null, data, actual, endStream);
|
||||||
dataLength = 0;
|
dataLength = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid timing writes that are already complete. This is likely to happen during the last iteration.
|
||||||
|
if (availabilityAwaitable == null && writeTask.IsCompleted)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minRate != null)
|
||||||
|
{
|
||||||
|
_timeoutControl.StartTimingWrite(minRate, actual);
|
||||||
|
}
|
||||||
|
|
||||||
// This awaitable releases continuations in FIFO order when the window updates.
|
// This awaitable releases continuations in FIFO order when the window updates.
|
||||||
// It should be very rare for a continuation to run without any availability.
|
// It should be very rare for a continuation to run without any availability.
|
||||||
if (availabilityAwaitable != null)
|
if (availabilityAwaitable != null)
|
||||||
|
|
@ -343,6 +359,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeTask;
|
await writeTask;
|
||||||
|
|
||||||
|
if (minRate != null)
|
||||||
|
{
|
||||||
|
_timeoutControl.StopTimingWrite();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that the application continuation isn't executed inline by ProcessWindowUpdateFrameAsync.
|
// Ensure that the application continuation isn't executed inline by ProcessWindowUpdateFrameAsync.
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Abort(ConnectionAbortedException abortReason)
|
// Review: This is called when a CancellationToken fires mid-write. In HTTP/1.x, this aborts the entire connection.
|
||||||
|
// Should we do that here?
|
||||||
|
void IHttpOutputAborter.Abort(ConnectionAbortedException abortReason)
|
||||||
{
|
{
|
||||||
Dispose();
|
Dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -206,14 +208,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
if (readResult.Buffer.Length > 0)
|
if (readResult.Buffer.Length > 0)
|
||||||
{
|
{
|
||||||
await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: false);
|
await _frameWriter.WriteDataAsync(_streamId, _flowControl, _stream.MinResponseDataRate, readResult.Buffer, endStream: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _frameWriter.WriteResponseTrailers(_streamId, _stream.Trailers);
|
await _frameWriter.WriteResponseTrailers(_streamId, _stream.Trailers);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: readResult.IsCompleted);
|
await _frameWriter.WriteDataAsync(_streamId, _flowControl, _stream.MinResponseDataRate, readResult.Buffer, endStream: readResult.IsCompleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
_dataPipe.Reader.AdvanceTo(readResult.Buffer.End);
|
_dataPipe.Reader.AdvanceTo(readResult.Buffer.End);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
return FlushAsync(minRate: null, count: 0, outputAborter: outputAborter, cancellationToken: cancellationToken);
|
return FlushAsync(minRate: null, count: 0, outputAborter: outputAborter, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task FlushAsync(MinDataRate minRate, long count)
|
||||||
|
{
|
||||||
|
return FlushAsync(minRate, count, outputAborter: null, cancellationToken: default);
|
||||||
|
}
|
||||||
|
|
||||||
public Task FlushAsync(MinDataRate minRate, long count, IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
|
public Task FlushAsync(MinDataRate minRate, long count, IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var flushValueTask = _writer.FlushAsync(cancellationToken);
|
var flushValueTask = _writer.FlushAsync(cancellationToken);
|
||||||
|
|
|
||||||
|
|
@ -10,77 +10,19 @@ using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
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.Http2;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
{
|
{
|
||||||
public class Http2ConnectionTests : Http2TestBase
|
public class Http2ConnectionTests : Http2TestBase
|
||||||
{
|
{
|
||||||
private static readonly IEnumerable<KeyValuePair<string, string>> _postRequestHeaders = new[]
|
|
||||||
{
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly IEnumerable<KeyValuePair<string, string>> _expectContinueRequestHeaders = new[]
|
|
||||||
{
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Authority, "127.0.0.1"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
|
|
||||||
new KeyValuePair<string, string>("expect", "100-continue"),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly IEnumerable<KeyValuePair<string, string>> _requestTrailers = new[]
|
|
||||||
{
|
|
||||||
new KeyValuePair<string, string>("trailer-one", "1"),
|
|
||||||
new KeyValuePair<string, string>("trailer-two", "2"),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly IEnumerable<KeyValuePair<string, string>> _oneContinuationRequestHeaders = new[]
|
|
||||||
{
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
|
||||||
new KeyValuePair<string, string>("a", _4kHeaderValue),
|
|
||||||
new KeyValuePair<string, string>("b", _4kHeaderValue),
|
|
||||||
new KeyValuePair<string, string>("c", _4kHeaderValue),
|
|
||||||
new KeyValuePair<string, string>("d", _4kHeaderValue)
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly IEnumerable<KeyValuePair<string, string>> _twoContinuationsRequestHeaders = new[]
|
|
||||||
{
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
|
||||||
new KeyValuePair<string, string>("a", _4kHeaderValue),
|
|
||||||
new KeyValuePair<string, string>("b", _4kHeaderValue),
|
|
||||||
new KeyValuePair<string, string>("c", _4kHeaderValue),
|
|
||||||
new KeyValuePair<string, string>("d", _4kHeaderValue),
|
|
||||||
new KeyValuePair<string, string>("e", _4kHeaderValue),
|
|
||||||
new KeyValuePair<string, string>("f", _4kHeaderValue),
|
|
||||||
new KeyValuePair<string, string>("g", _4kHeaderValue),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly byte[] _helloBytes = Encoding.ASCII.GetBytes("hello");
|
|
||||||
private static readonly byte[] _worldBytes = Encoding.ASCII.GetBytes("world");
|
|
||||||
private static readonly byte[] _helloWorldBytes = Encoding.ASCII.GetBytes("hello, world");
|
|
||||||
private static readonly byte[] _noData = new byte[0];
|
|
||||||
private static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', Http2PeerSettings.MinAllowedMaxFrameSize));
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Frame_Received_OverMaxSize_FrameError()
|
public async Task Frame_Received_OverMaxSize_FrameError()
|
||||||
{
|
{
|
||||||
|
|
@ -102,8 +44,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
public async Task ServerSettings_ChangesRequestMaxFrameSize()
|
public async Task ServerSettings_ChangesRequestMaxFrameSize()
|
||||||
{
|
{
|
||||||
var length = Http2PeerSettings.MinAllowedMaxFrameSize + 10;
|
var length = Http2PeerSettings.MinAllowedMaxFrameSize + 10;
|
||||||
_connectionContext.ServiceContext.ServerOptions.Limits.Http2.MaxFrameSize = length;
|
_serviceContext.ServerOptions.Limits.Http2.MaxFrameSize = length;
|
||||||
_connection = new Http2Connection(_connectionContext);
|
|
||||||
|
|
||||||
await InitializeConnectionAsync(_echoApplication, expectedSettingsCount: 4);
|
await InitializeConnectionAsync(_echoApplication, expectedSettingsCount: 4);
|
||||||
|
|
||||||
|
|
@ -187,9 +128,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DATA_Received_GreaterThanInitialWindowSize_ReadByStream()
|
public async Task DATA_Received_GreaterThanInitialWindowSize_ReadByStream()
|
||||||
{
|
{
|
||||||
var initialStreamWindowSize = _connectionContext.ServiceContext.ServerOptions.Limits.Http2.InitialStreamWindowSize;
|
var initialStreamWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialStreamWindowSize;
|
||||||
var framesInStreamWindow = initialStreamWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
var framesInStreamWindow = initialStreamWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
||||||
var initialConnectionWindowSize = _connectionContext.ServiceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
var initialConnectionWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
||||||
var framesInConnectionWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
var framesInConnectionWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
||||||
|
|
||||||
// Grow the client stream windows so no stream WINDOW_UPDATEs need to be sent.
|
// Grow the client stream windows so no stream WINDOW_UPDATEs need to be sent.
|
||||||
|
|
@ -288,9 +229,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DATA_Received_RightAtWindowLimit_DoesNotPausePipe()
|
public async Task DATA_Received_RightAtWindowLimit_DoesNotPausePipe()
|
||||||
{
|
{
|
||||||
var initialStreamWindowSize = _connectionContext.ServiceContext.ServerOptions.Limits.Http2.InitialStreamWindowSize;
|
var initialStreamWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialStreamWindowSize;
|
||||||
var framesInStreamWindow = initialStreamWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
var framesInStreamWindow = initialStreamWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
||||||
var initialConnectionWindowSize = _connectionContext.ServiceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
var initialConnectionWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
||||||
var framesInConnectionWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
var framesInConnectionWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
||||||
|
|
||||||
await InitializeConnectionAsync(_waitForAbortApplication);
|
await InitializeConnectionAsync(_waitForAbortApplication);
|
||||||
|
|
@ -416,8 +357,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DATA_Received_Multiplexed_GreaterThanInitialWindowSize_ReadByStream()
|
public async Task DATA_Received_Multiplexed_GreaterThanInitialWindowSize_ReadByStream()
|
||||||
{
|
{
|
||||||
var initialStreamWindowSize = _connectionContext.ServiceContext.ServerOptions.Limits.Http2.InitialStreamWindowSize;
|
var initialStreamWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialStreamWindowSize;
|
||||||
var initialConnectionWindowSize = _connectionContext.ServiceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
var initialConnectionWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
||||||
var framesInStreamWindow = initialStreamWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
var framesInStreamWindow = initialStreamWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
||||||
var framesInConnectionWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
var framesInConnectionWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
||||||
|
|
||||||
|
|
@ -630,7 +571,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[InlineData(255)]
|
[InlineData(255)]
|
||||||
public async Task DATA_Received_WithPadding_CountsTowardsInputFlowControl(byte padLength)
|
public async Task DATA_Received_WithPadding_CountsTowardsInputFlowControl(byte padLength)
|
||||||
{
|
{
|
||||||
var initialWindowSize = _connectionContext.ServiceContext.ServerOptions.Limits.Http2.InitialStreamWindowSize;
|
var initialWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialStreamWindowSize;
|
||||||
var framesInWindow = initialWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
var framesInWindow = initialWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
||||||
var maxDataMinusPadding = _maxData.AsMemory(0, _maxData.Length - padLength - 1);
|
var maxDataMinusPadding = _maxData.AsMemory(0, _maxData.Length - padLength - 1);
|
||||||
|
|
||||||
|
|
@ -699,7 +640,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DATA_Received_ButNotConsumedByApp_CountsTowardsInputFlowControl()
|
public async Task DATA_Received_ButNotConsumedByApp_CountsTowardsInputFlowControl()
|
||||||
{
|
{
|
||||||
var initialConnectionWindowSize = _connectionContext.ServiceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
var initialConnectionWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
||||||
var framesConnectionInWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
var framesConnectionInWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
||||||
|
|
||||||
await InitializeConnectionAsync(_noopApplication);
|
await InitializeConnectionAsync(_noopApplication);
|
||||||
|
|
@ -925,7 +866,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DATA_Received_NoStreamWindowSpace_ConnectionError()
|
public async Task DATA_Received_NoStreamWindowSpace_ConnectionError()
|
||||||
{
|
{
|
||||||
var initialWindowSize = _connectionContext.ServiceContext.ServerOptions.Limits.Http2.InitialStreamWindowSize;
|
var initialWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialStreamWindowSize;
|
||||||
var framesInWindow = (initialWindowSize / Http2PeerSettings.DefaultMaxFrameSize) + 1; // Round up to overflow the window
|
var framesInWindow = (initialWindowSize / Http2PeerSettings.DefaultMaxFrameSize) + 1; // Round up to overflow the window
|
||||||
|
|
||||||
await InitializeConnectionAsync(_waitForAbortApplication);
|
await InitializeConnectionAsync(_waitForAbortApplication);
|
||||||
|
|
@ -947,7 +888,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DATA_Received_NoConnectionWindowSpace_ConnectionError()
|
public async Task DATA_Received_NoConnectionWindowSpace_ConnectionError()
|
||||||
{
|
{
|
||||||
var initialWindowSize = _connectionContext.ServiceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
var initialWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
||||||
var framesInWindow = initialWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
var framesInWindow = initialWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
||||||
|
|
||||||
await InitializeConnectionAsync(_waitForAbortApplication);
|
await InitializeConnectionAsync(_waitForAbortApplication);
|
||||||
|
|
@ -1314,6 +1255,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HEADERS_OverMaxStreamLimit_Refused()
|
public async Task HEADERS_OverMaxStreamLimit_Refused()
|
||||||
{
|
{
|
||||||
|
CreateConnection();
|
||||||
|
|
||||||
_connection.ServerSettings.MaxConcurrentStreams = 1;
|
_connection.ServerSettings.MaxConcurrentStreams = 1;
|
||||||
|
|
||||||
var requestBlocker = new TaskCompletionSource<object>();
|
var requestBlocker = new TaskCompletionSource<object>();
|
||||||
|
|
@ -2093,7 +2036,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RST_STREAM_Received_ReturnsSpaceToConnectionInputFlowControlWindow()
|
public async Task RST_STREAM_Received_ReturnsSpaceToConnectionInputFlowControlWindow()
|
||||||
{
|
{
|
||||||
var initialConnectionWindowSize = _connectionContext.ServiceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
var initialConnectionWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
|
||||||
var framesInConnectionWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
var framesInConnectionWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
|
||||||
|
|
||||||
await InitializeConnectionAsync(_waitForAbortApplication);
|
await InitializeConnectionAsync(_waitForAbortApplication);
|
||||||
|
|
@ -2298,6 +2241,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SETTINGS_KestrelDefaults_Sent()
|
public async Task SETTINGS_KestrelDefaults_Sent()
|
||||||
{
|
{
|
||||||
|
CreateConnection();
|
||||||
|
|
||||||
_connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(_noopApplication));
|
_connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(_noopApplication));
|
||||||
|
|
||||||
await SendPreambleAsync().ConfigureAwait(false);
|
await SendPreambleAsync().ConfigureAwait(false);
|
||||||
|
|
@ -2342,6 +2287,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SETTINGS_Custom_Sent()
|
public async Task SETTINGS_Custom_Sent()
|
||||||
{
|
{
|
||||||
|
CreateConnection();
|
||||||
|
|
||||||
_connection.ServerSettings.MaxConcurrentStreams = 1;
|
_connection.ServerSettings.MaxConcurrentStreams = 1;
|
||||||
_connection.ServerSettings.MaxHeaderListSize = 4 * 1024;
|
_connection.ServerSettings.MaxHeaderListSize = 4 * 1024;
|
||||||
_connection.ServerSettings.InitialWindowSize = 1024 * 1024 * 10;
|
_connection.ServerSettings.InitialWindowSize = 1024 * 1024 * 10;
|
||||||
|
|
@ -2522,6 +2469,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SETTINGS_Received_ChangesAllowedResponseMaxFrameSize()
|
public async Task SETTINGS_Received_ChangesAllowedResponseMaxFrameSize()
|
||||||
{
|
{
|
||||||
|
CreateConnection();
|
||||||
|
|
||||||
_connection.ServerSettings.MaxFrameSize = Http2PeerSettings.MaxAllowedMaxFrameSize;
|
_connection.ServerSettings.MaxFrameSize = Http2PeerSettings.MaxAllowedMaxFrameSize;
|
||||||
// This includes the default response headers such as :status, etc
|
// This includes the default response headers such as :status, etc
|
||||||
var defaultResponseHeaderLength = 37;
|
var defaultResponseHeaderLength = 37;
|
||||||
|
|
@ -2578,6 +2527,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
public async Task SETTINGS_Received_ClientMaxFrameSizeCannotExceedServerMaxFrameSize()
|
public async Task SETTINGS_Received_ClientMaxFrameSizeCannotExceedServerMaxFrameSize()
|
||||||
{
|
{
|
||||||
var serverMaxFrame = Http2PeerSettings.MinAllowedMaxFrameSize + 1024;
|
var serverMaxFrame = Http2PeerSettings.MinAllowedMaxFrameSize + 1024;
|
||||||
|
|
||||||
|
CreateConnection();
|
||||||
|
|
||||||
_connection.ServerSettings.MaxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize + 1024;
|
_connection.ServerSettings.MaxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize + 1024;
|
||||||
var clientMaxFrame = serverMaxFrame + 1024 * 5;
|
var clientMaxFrame = serverMaxFrame + 1024 * 5;
|
||||||
_clientSettings.MaxFrameSize = (uint)clientMaxFrame;
|
_clientSettings.MaxFrameSize = (uint)clientMaxFrame;
|
||||||
|
|
@ -3749,6 +3701,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task IgnoreNewStreamsDuringClosedConnection()
|
public async Task IgnoreNewStreamsDuringClosedConnection()
|
||||||
{
|
{
|
||||||
|
// Remove callback that completes _pair.Application.Output on abort.
|
||||||
|
_mockConnectionContext.Reset();
|
||||||
|
|
||||||
await InitializeConnectionAsync(_echoApplication);
|
await InitializeConnectionAsync(_echoApplication);
|
||||||
|
|
||||||
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
|
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
|
||||||
|
|
@ -3766,6 +3721,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IOExceptionDuringFrameProcessingLoggedAsInfo()
|
public void IOExceptionDuringFrameProcessingLoggedAsInfo()
|
||||||
{
|
{
|
||||||
|
CreateConnection();
|
||||||
|
|
||||||
var ioException = new IOException();
|
var ioException = new IOException();
|
||||||
_pair.Application.Output.Complete(ioException);
|
_pair.Application.Output.Complete(ioException);
|
||||||
|
|
||||||
|
|
@ -3781,6 +3738,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UnexpectedExceptionDuringFrameProcessingLoggedAWarning()
|
public void UnexpectedExceptionDuringFrameProcessingLoggedAWarning()
|
||||||
{
|
{
|
||||||
|
CreateConnection();
|
||||||
|
|
||||||
var exception = new Exception();
|
var exception = new Exception();
|
||||||
_pair.Application.Output.Complete(exception);
|
_pair.Application.Output.Complete(exception);
|
||||||
|
|
||||||
|
|
@ -4000,62 +3959,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
CoreStrings.FormatHttp2ErrorStreamClosed(finalFrameType, 1));
|
CoreStrings.FormatHttp2ErrorStreamClosed(finalFrameType, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(Http2FrameType.DATA)]
|
|
||||||
[InlineData(Http2FrameType.HEADERS)]
|
|
||||||
public async Task AbortedStream_ResetsAndDrainsRequest_RefusesFramesAfterCooldownExpires(Http2FrameType finalFrameType)
|
|
||||||
{
|
|
||||||
var mockSystemClock = new MockSystemClock();
|
|
||||||
_connectionContext.ServiceContext.SystemClock = mockSystemClock;
|
|
||||||
|
|
||||||
var headers = new[]
|
|
||||||
{
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
|
||||||
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
|
||||||
new KeyValuePair<string, string>(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.");
|
|
||||||
|
|
||||||
// There's a race when the appfunc is exiting about how soon it unregisters the stream.
|
|
||||||
for (var i = 0; i < 10; i++)
|
|
||||||
{
|
|
||||||
await SendDataAsync(1, new byte[100], endStream: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Just short of the timeout
|
|
||||||
mockSystemClock.UtcNow += Constants.RequestBodyDrainTimeout - TimeSpan.FromTicks(1);
|
|
||||||
(_connection as IRequestProcessor).Tick(mockSystemClock.UtcNow);
|
|
||||||
|
|
||||||
// Still fine
|
|
||||||
await SendDataAsync(1, new byte[100], endStream: false);
|
|
||||||
|
|
||||||
// Just past the timeout
|
|
||||||
mockSystemClock.UtcNow += TimeSpan.FromTicks(2);
|
|
||||||
(_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: true);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Http2FrameType.HEADERS:
|
|
||||||
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM | Http2HeadersFrameFlags.END_HEADERS, _requestTrailers);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new NotImplementedException(finalFrameType.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
await WaitForConnectionErrorAsync<Http2ConnectionErrorException>(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.STREAM_CLOSED,
|
|
||||||
CoreStrings.FormatHttp2ErrorStreamClosed(finalFrameType, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TheoryData<byte[]> UpperCaseHeaderNameData
|
public static TheoryData<byte[]> UpperCaseHeaderNameData
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -1065,7 +1065,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task MaxRequestBodySize_ContentLengthUnder_200()
|
public async Task MaxRequestBodySize_ContentLengthUnder_200()
|
||||||
{
|
{
|
||||||
_connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 15;
|
_serviceContext.ServerOptions.Limits.MaxRequestBodySize = 15;
|
||||||
var headers = new[]
|
var headers = new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||||
|
|
@ -1108,7 +1108,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
public async Task MaxRequestBodySize_ContentLengthOver_413()
|
public async Task MaxRequestBodySize_ContentLengthOver_413()
|
||||||
{
|
{
|
||||||
BadHttpRequestException exception = null;
|
BadHttpRequestException exception = null;
|
||||||
_connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 10;
|
_serviceContext.ServerOptions.Limits.MaxRequestBodySize = 10;
|
||||||
var headers = new[]
|
var headers = new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||||
|
|
@ -1156,7 +1156,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task MaxRequestBodySize_NoContentLength_Under_200()
|
public async Task MaxRequestBodySize_NoContentLength_Under_200()
|
||||||
{
|
{
|
||||||
_connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 15;
|
_serviceContext.ServerOptions.Limits.MaxRequestBodySize = 15;
|
||||||
var headers = new[]
|
var headers = new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||||
|
|
@ -1198,7 +1198,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
public async Task MaxRequestBodySize_NoContentLength_Over_413()
|
public async Task MaxRequestBodySize_NoContentLength_Over_413()
|
||||||
{
|
{
|
||||||
BadHttpRequestException exception = null;
|
BadHttpRequestException exception = null;
|
||||||
_connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 10;
|
_serviceContext.ServerOptions.Limits.MaxRequestBodySize = 10;
|
||||||
var headers = new[]
|
var headers = new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||||
|
|
@ -1247,7 +1247,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
public async Task MaxRequestBodySize_AppCanLowerLimit(bool includeContentLength)
|
public async Task MaxRequestBodySize_AppCanLowerLimit(bool includeContentLength)
|
||||||
{
|
{
|
||||||
BadHttpRequestException exception = null;
|
BadHttpRequestException exception = null;
|
||||||
_connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 20;
|
_serviceContext.ServerOptions.Limits.MaxRequestBodySize = 20;
|
||||||
var headers = new[]
|
var headers = new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||||
|
|
@ -1305,7 +1305,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[InlineData(false)]
|
[InlineData(false)]
|
||||||
public async Task MaxRequestBodySize_AppCanRaiseLimit(bool includeContentLength)
|
public async Task MaxRequestBodySize_AppCanRaiseLimit(bool includeContentLength)
|
||||||
{
|
{
|
||||||
_connectionContext.ServiceContext.ServerOptions.Limits.MaxRequestBodySize = 10;
|
_serviceContext.ServerOptions.Limits.MaxRequestBodySize = 10;
|
||||||
var headers = new[]
|
var headers = new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using System.IO;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
|
|
@ -48,8 +49,63 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
new KeyValuePair<string, string>("upgrade-insecure-requests", "1"),
|
new KeyValuePair<string, string>("upgrade-insecure-requests", "1"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
protected static readonly IEnumerable<KeyValuePair<string, string>> _postRequestHeaders = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
||||||
|
};
|
||||||
|
|
||||||
|
protected static readonly IEnumerable<KeyValuePair<string, string>> _expectContinueRequestHeaders = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Authority, "127.0.0.1"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
|
||||||
|
new KeyValuePair<string, string>("expect", "100-continue"),
|
||||||
|
};
|
||||||
|
|
||||||
|
protected static readonly IEnumerable<KeyValuePair<string, string>> _requestTrailers = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>("trailer-one", "1"),
|
||||||
|
new KeyValuePair<string, string>("trailer-two", "2"),
|
||||||
|
};
|
||||||
|
|
||||||
|
protected static readonly IEnumerable<KeyValuePair<string, string>> _oneContinuationRequestHeaders = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
||||||
|
new KeyValuePair<string, string>("a", _4kHeaderValue),
|
||||||
|
new KeyValuePair<string, string>("b", _4kHeaderValue),
|
||||||
|
new KeyValuePair<string, string>("c", _4kHeaderValue),
|
||||||
|
new KeyValuePair<string, string>("d", _4kHeaderValue)
|
||||||
|
};
|
||||||
|
|
||||||
|
protected static readonly IEnumerable<KeyValuePair<string, string>> _twoContinuationsRequestHeaders = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
||||||
|
new KeyValuePair<string, string>("a", _4kHeaderValue),
|
||||||
|
new KeyValuePair<string, string>("b", _4kHeaderValue),
|
||||||
|
new KeyValuePair<string, string>("c", _4kHeaderValue),
|
||||||
|
new KeyValuePair<string, string>("d", _4kHeaderValue),
|
||||||
|
new KeyValuePair<string, string>("e", _4kHeaderValue),
|
||||||
|
new KeyValuePair<string, string>("f", _4kHeaderValue),
|
||||||
|
new KeyValuePair<string, string>("g", _4kHeaderValue),
|
||||||
|
};
|
||||||
|
|
||||||
|
protected static readonly byte[] _helloBytes = Encoding.ASCII.GetBytes("hello");
|
||||||
|
protected static readonly byte[] _worldBytes = Encoding.ASCII.GetBytes("world");
|
||||||
|
protected static readonly byte[] _helloWorldBytes = Encoding.ASCII.GetBytes("hello, world");
|
||||||
|
protected static readonly byte[] _noData = new byte[0];
|
||||||
|
protected static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', Http2PeerSettings.MinAllowedMaxFrameSize));
|
||||||
|
|
||||||
private readonly MemoryPool<byte> _memoryPool = KestrelMemoryPool.Create();
|
private readonly MemoryPool<byte> _memoryPool = KestrelMemoryPool.Create();
|
||||||
internal readonly DuplexPipe.DuplexPipePair _pair;
|
|
||||||
|
|
||||||
protected readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();
|
protected readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();
|
||||||
protected readonly HPackEncoder _hpackEncoder = new HPackEncoder();
|
protected readonly HPackEncoder _hpackEncoder = new HPackEncoder();
|
||||||
|
|
@ -57,6 +113,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
private readonly byte[] _headerEncodingBuffer = new byte[Http2PeerSettings.MinAllowedMaxFrameSize];
|
private readonly byte[] _headerEncodingBuffer = new byte[Http2PeerSettings.MinAllowedMaxFrameSize];
|
||||||
|
|
||||||
protected readonly TimeoutControl _timeoutControl;
|
protected readonly TimeoutControl _timeoutControl;
|
||||||
|
protected readonly Mock<IKestrelTrace> _mockKestrelTrace = new Mock<IKestrelTrace>();
|
||||||
protected readonly Mock<ConnectionContext> _mockConnectionContext = new Mock<ConnectionContext>();
|
protected readonly Mock<ConnectionContext> _mockConnectionContext = new Mock<ConnectionContext>();
|
||||||
protected readonly Mock<ITimeoutHandler> _mockTimeoutHandler = new Mock<ITimeoutHandler>();
|
protected readonly Mock<ITimeoutHandler> _mockTimeoutHandler = new Mock<ITimeoutHandler>();
|
||||||
protected readonly Mock<MockTimeoutControlBase> _mockTimeoutControl;
|
protected readonly Mock<MockTimeoutControlBase> _mockTimeoutControl;
|
||||||
|
|
@ -84,33 +141,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
protected readonly RequestDelegate _echoPath;
|
protected readonly RequestDelegate _echoPath;
|
||||||
protected readonly RequestDelegate _appAbort;
|
protected readonly RequestDelegate _appAbort;
|
||||||
|
|
||||||
protected HttpConnectionContext _connectionContext;
|
protected TestServiceContext _serviceContext;
|
||||||
|
|
||||||
|
internal DuplexPipe.DuplexPipePair _pair;
|
||||||
protected Http2Connection _connection;
|
protected Http2Connection _connection;
|
||||||
protected Task _connectionTask;
|
protected Task _connectionTask;
|
||||||
|
|
||||||
public Http2TestBase()
|
public Http2TestBase()
|
||||||
{
|
{
|
||||||
// Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing
|
|
||||||
// Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make
|
|
||||||
// it easier to verify request frames are processed correctly immediately after sending the them.
|
|
||||||
var inputPipeOptions = new PipeOptions(
|
|
||||||
pool: _memoryPool,
|
|
||||||
readerScheduler: PipeScheduler.Inline,
|
|
||||||
writerScheduler: PipeScheduler.ThreadPool,
|
|
||||||
useSynchronizationContext: false
|
|
||||||
);
|
|
||||||
var outputPipeOptions = new PipeOptions(
|
|
||||||
pool: _memoryPool,
|
|
||||||
readerScheduler: PipeScheduler.ThreadPool,
|
|
||||||
writerScheduler: PipeScheduler.Inline,
|
|
||||||
useSynchronizationContext: false
|
|
||||||
);
|
|
||||||
|
|
||||||
_pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
|
|
||||||
_hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize);
|
_hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize);
|
||||||
|
|
||||||
_timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object);
|
_timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object);
|
||||||
_mockTimeoutControl = new Mock<MockTimeoutControlBase>(_timeoutControl) { CallBase = true };
|
_mockTimeoutControl = new Mock<MockTimeoutControlBase>(_timeoutControl) { CallBase = true };
|
||||||
|
_timeoutControl.Debugger = Mock.Of<IDebugger>();
|
||||||
|
|
||||||
|
_mockKestrelTrace
|
||||||
|
.Setup(m => m.Http2ConnectionClosing(It.IsAny<string>()))
|
||||||
|
.Callback(() => _closingStateReached.SetResult(null));
|
||||||
|
_mockKestrelTrace
|
||||||
|
.Setup(m => m.Http2ConnectionClosed(It.IsAny<string>(), It.IsAny<int>()))
|
||||||
|
.Callback(() => _closedStateReached.SetResult(null));
|
||||||
|
|
||||||
|
_mockConnectionContext.Setup(c => c.Abort(It.IsAny<ConnectionAbortedException>())).Callback<ConnectionAbortedException>(ex =>
|
||||||
|
{
|
||||||
|
// Emulate transport abort so the _connectionTask completes.
|
||||||
|
_pair.Application.Output.Complete(ex);
|
||||||
|
});
|
||||||
|
|
||||||
_noopApplication = context => Task.CompletedTask;
|
_noopApplication = context => Task.CompletedTask;
|
||||||
|
|
||||||
|
|
@ -290,38 +346,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
{
|
{
|
||||||
base.Initialize(methodInfo, testMethodArguments, testOutputHelper);
|
base.Initialize(methodInfo, testMethodArguments, testOutputHelper);
|
||||||
|
|
||||||
var mockKestrelTrace = new Mock<IKestrelTrace>();
|
_serviceContext = new TestServiceContext(LoggerFactory, _mockKestrelTrace.Object)
|
||||||
mockKestrelTrace
|
|
||||||
.Setup(m => m.Http2ConnectionClosing(It.IsAny<string>()))
|
|
||||||
.Callback(() => _closingStateReached.SetResult(null));
|
|
||||||
mockKestrelTrace
|
|
||||||
.Setup(m => m.Http2ConnectionClosed(It.IsAny<string>(), It.IsAny<int>()))
|
|
||||||
.Callback(() => _closedStateReached.SetResult(null));
|
|
||||||
|
|
||||||
_connectionContext = new HttpConnectionContext
|
|
||||||
{
|
{
|
||||||
ConnectionContext = _mockConnectionContext.Object,
|
Scheduler = PipeScheduler.Inline
|
||||||
ConnectionFeatures = new FeatureCollection(),
|
|
||||||
ServiceContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object),
|
|
||||||
MemoryPool = _memoryPool,
|
|
||||||
Transport = _pair.Transport,
|
|
||||||
TimeoutControl = _mockTimeoutControl.Object
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_connection = new Http2Connection(_connectionContext);
|
|
||||||
|
|
||||||
var httpConnection = new HttpConnection(_connectionContext);
|
|
||||||
httpConnection.Initialize(_connection);
|
|
||||||
_mockTimeoutHandler.Setup(h => h.OnTimeout(It.IsAny<TimeoutReason>()))
|
|
||||||
.Callback<TimeoutReason>(r => httpConnection.OnTimeout(r));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
_pair.Application.Input.Complete();
|
_pair.Application?.Input.Complete();
|
||||||
_pair.Application.Output.Complete();
|
_pair.Application?.Output.Complete();
|
||||||
_pair.Transport.Input.Complete();
|
_pair.Transport?.Input.Complete();
|
||||||
_pair.Transport.Output.Complete();
|
_pair.Transport?.Output.Complete();
|
||||||
_memoryPool.Dispose();
|
_memoryPool.Dispose();
|
||||||
|
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
|
|
@ -332,8 +368,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
|
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void CreateConnection()
|
||||||
|
{
|
||||||
|
var limits = _serviceContext.ServerOptions.Limits;
|
||||||
|
|
||||||
|
// Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing
|
||||||
|
// Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make
|
||||||
|
// it easier to verify request frames are processed correctly immediately after sending the them.
|
||||||
|
var inputPipeOptions = ConnectionDispatcher.GetInputPipeOptions(_serviceContext, _memoryPool, PipeScheduler.ThreadPool);
|
||||||
|
var outputPipeOptions = ConnectionDispatcher.GetOutputPipeOptions(_serviceContext, _memoryPool, PipeScheduler.ThreadPool);
|
||||||
|
|
||||||
|
_pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
|
||||||
|
|
||||||
|
var httpConnectionContext = new HttpConnectionContext
|
||||||
|
{
|
||||||
|
ConnectionContext = _mockConnectionContext.Object,
|
||||||
|
ConnectionFeatures = new FeatureCollection(),
|
||||||
|
ServiceContext = _serviceContext,
|
||||||
|
MemoryPool = _memoryPool,
|
||||||
|
Transport = _pair.Transport,
|
||||||
|
TimeoutControl = _mockTimeoutControl.Object
|
||||||
|
};
|
||||||
|
|
||||||
|
_connection = new Http2Connection(httpConnectionContext);
|
||||||
|
|
||||||
|
var httpConnection = new HttpConnection(httpConnectionContext);
|
||||||
|
httpConnection.Initialize(_connection);
|
||||||
|
_mockTimeoutHandler.Setup(h => h.OnTimeout(It.IsAny<TimeoutReason>()))
|
||||||
|
.Callback<TimeoutReason>(r => httpConnection.OnTimeout(r));
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task InitializeConnectionAsync(RequestDelegate application, int expectedSettingsCount = 3)
|
protected async Task InitializeConnectionAsync(RequestDelegate application, int expectedSettingsCount = 3)
|
||||||
{
|
{
|
||||||
|
if (_connection == null)
|
||||||
|
{
|
||||||
|
CreateConnection();
|
||||||
|
}
|
||||||
|
|
||||||
_connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(application));
|
_connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(application));
|
||||||
|
|
||||||
await SendPreambleAsync().ConfigureAwait(false);
|
await SendPreambleAsync().ConfigureAwait(false);
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -17,8 +19,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HEADERS_NotReceivedInitially_WithinKeepAliveTimeout_ClosesConnection()
|
public async Task HEADERS_NotReceivedInitially_WithinKeepAliveTimeout_ClosesConnection()
|
||||||
{
|
{
|
||||||
var mockSystemClock = new MockSystemClock();
|
var mockSystemClock = _serviceContext.MockSystemClock;
|
||||||
var limits = _connectionContext.ServiceContext.ServerOptions.Limits;
|
var limits = _serviceContext.ServerOptions.Limits;
|
||||||
|
|
||||||
_timeoutControl.Initialize(mockSystemClock.UtcNow);
|
_timeoutControl.Initialize(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
|
@ -40,8 +42,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HEADERS_NotReceivedAfterFirstRequest_WithinKeepAliveTimeout_ClosesConnection()
|
public async Task HEADERS_NotReceivedAfterFirstRequest_WithinKeepAliveTimeout_ClosesConnection()
|
||||||
{
|
{
|
||||||
var mockSystemClock = new MockSystemClock();
|
var mockSystemClock = _serviceContext.MockSystemClock;
|
||||||
var limits = _connectionContext.ServiceContext.ServerOptions.Limits;
|
var limits = _serviceContext.ServerOptions.Limits;
|
||||||
|
|
||||||
_timeoutControl.Initialize(mockSystemClock.UtcNow);
|
_timeoutControl.Initialize(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
|
@ -97,14 +99,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HEADERS_ReceivedWithoutAllCONTINUATIONs_WithinRequestHeadersTimeout_AbortsConnection()
|
public async Task HEADERS_ReceivedWithoutAllCONTINUATIONs_WithinRequestHeadersTimeout_AbortsConnection()
|
||||||
{
|
{
|
||||||
var mockSystemClock = new MockSystemClock();
|
var mockSystemClock = _serviceContext.MockSystemClock;
|
||||||
var limits = _connectionContext.ServiceContext.ServerOptions.Limits;
|
var limits = _serviceContext.ServerOptions.Limits;;
|
||||||
|
|
||||||
_mockConnectionContext.Setup(c => c.Abort(It.IsAny<ConnectionAbortedException>())).Callback<ConnectionAbortedException>(ex =>
|
|
||||||
{
|
|
||||||
// Emulate transport abort so the _connectionTask completes.
|
|
||||||
_pair.Application.Output.Complete(ex);
|
|
||||||
});
|
|
||||||
|
|
||||||
_timeoutControl.Initialize(mockSystemClock.UtcNow);
|
_timeoutControl.Initialize(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
|
@ -139,8 +135,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResponseDrain_SlowerThanMinimumDataRate_AbortsConnection()
|
public async Task ResponseDrain_SlowerThanMinimumDataRate_AbortsConnection()
|
||||||
{
|
{
|
||||||
var mockSystemClock = new MockSystemClock();
|
var mockSystemClock = _serviceContext.MockSystemClock;
|
||||||
var limits = _connectionContext.ServiceContext.ServerOptions.Limits;
|
var limits = _serviceContext.ServerOptions.Limits;
|
||||||
|
|
||||||
_timeoutControl.Initialize(mockSystemClock.UtcNow);
|
_timeoutControl.Initialize(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
|
@ -166,5 +162,314 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
_mockConnectionContext.Verify(c =>c.Abort(It.Is<ConnectionAbortedException>(e =>
|
_mockConnectionContext.Verify(c =>c.Abort(It.Is<ConnectionAbortedException>(e =>
|
||||||
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
|
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(Http2FrameType.DATA)]
|
||||||
|
[InlineData(Http2FrameType.HEADERS)]
|
||||||
|
public async Task AbortedStream_ResetsAndDrainsRequest_RefusesFramesAfterCooldownExpires(Http2FrameType finalFrameType)
|
||||||
|
{
|
||||||
|
var mockSystemClock = _serviceContext.MockSystemClock;
|
||||||
|
|
||||||
|
var headers = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||||
|
new KeyValuePair<string, string>(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.");
|
||||||
|
|
||||||
|
// There's a race when the appfunc is exiting about how soon it unregisters the stream.
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
await SendDataAsync(1, new byte[100], endStream: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just short of the timeout
|
||||||
|
mockSystemClock.UtcNow += Constants.RequestBodyDrainTimeout;
|
||||||
|
(_connection as IRequestProcessor).Tick(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
// Still fine
|
||||||
|
await SendDataAsync(1, new byte[100], endStream: false);
|
||||||
|
|
||||||
|
// Just past the timeout
|
||||||
|
mockSystemClock.UtcNow += 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: true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Http2FrameType.HEADERS:
|
||||||
|
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM | Http2HeadersFrameFlags.END_HEADERS, _requestTrailers);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException(finalFrameType.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
await WaitForConnectionErrorAsync<Http2ConnectionErrorException>(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.STREAM_CLOSED,
|
||||||
|
CoreStrings.FormatHttp2ErrorStreamClosed(finalFrameType, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnSmallWrite_AbortsConnectionAfterGracePeriod()
|
||||||
|
{
|
||||||
|
var mockSystemClock = _serviceContext.MockSystemClock;
|
||||||
|
var limits = _serviceContext.ServerOptions.Limits;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Don't read data frame to induce "socket" backpressure.
|
||||||
|
mockSystemClock.UtcNow += limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval;
|
||||||
|
_timeoutControl.Tick(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
|
||||||
|
|
||||||
|
mockSystemClock.UtcNow += TimeSpan.FromTicks(1);
|
||||||
|
_timeoutControl.Tick(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
_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<ConnectionAbortedException>(
|
||||||
|
ignoreNonGoAwayFrames: false,
|
||||||
|
expectedLastStreamId: 1,
|
||||||
|
Http2ErrorCode.INTERNAL_ERROR,
|
||||||
|
null);
|
||||||
|
|
||||||
|
_mockConnectionContext.Verify(c =>c.Abort(It.Is<ConnectionAbortedException>(e =>
|
||||||
|
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnLargeWrite_AbortsConnectionAfterRateTimeout()
|
||||||
|
{
|
||||||
|
var mockSystemClock = _serviceContext.MockSystemClock;
|
||||||
|
var limits = _serviceContext.ServerOptions.Limits;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
var timeToWriteMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinResponseDataRate.BytesPerSecond);
|
||||||
|
|
||||||
|
// Don't read data frame to induce "socket" backpressure.
|
||||||
|
mockSystemClock.UtcNow += timeToWriteMaxData + Heartbeat.Interval;
|
||||||
|
_timeoutControl.Tick(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
|
||||||
|
|
||||||
|
mockSystemClock.UtcNow += TimeSpan.FromTicks(1);
|
||||||
|
_timeoutControl.Tick(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
_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: _maxData.Length,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
|
||||||
|
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
|
||||||
|
ignoreNonGoAwayFrames: false,
|
||||||
|
expectedLastStreamId: 1,
|
||||||
|
Http2ErrorCode.INTERNAL_ERROR,
|
||||||
|
null);
|
||||||
|
|
||||||
|
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
|
||||||
|
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DATA_Sent_TooSlowlyDueToFlowControlOnSmallWrite_AbortsConnectionAfterGracePeriod()
|
||||||
|
{
|
||||||
|
var mockSystemClock = _serviceContext.MockSystemClock;
|
||||||
|
var limits = _serviceContext.ServerOptions.Limits;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Don't send WINDOW_UPDATE to induce flow-control backpressure
|
||||||
|
mockSystemClock.UtcNow += limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval;
|
||||||
|
_timeoutControl.Tick(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
|
||||||
|
|
||||||
|
mockSystemClock.UtcNow += TimeSpan.FromTicks(1);
|
||||||
|
_timeoutControl.Tick(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once);
|
||||||
|
|
||||||
|
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
|
||||||
|
ignoreNonGoAwayFrames: false,
|
||||||
|
expectedLastStreamId: 1,
|
||||||
|
Http2ErrorCode.INTERNAL_ERROR,
|
||||||
|
null);
|
||||||
|
|
||||||
|
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
|
||||||
|
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DATA_Sent_TooSlowlyDueToOutputFlowControlOnLargeWrite_AbortsConnectionAfterRateTimeout()
|
||||||
|
{
|
||||||
|
var mockSystemClock = _serviceContext.MockSystemClock;
|
||||||
|
var limits = _serviceContext.ServerOptions.Limits;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
var timeToWriteMaxData = TimeSpan.FromSeconds(_clientSettings.InitialWindowSize / limits.MinResponseDataRate.BytesPerSecond);
|
||||||
|
|
||||||
|
// Don't send WINDOW_UPDATE to induce flow-control backpressure
|
||||||
|
mockSystemClock.UtcNow += timeToWriteMaxData + Heartbeat.Interval;
|
||||||
|
_timeoutControl.Tick(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
|
||||||
|
|
||||||
|
mockSystemClock.UtcNow += TimeSpan.FromTicks(1);
|
||||||
|
_timeoutControl.Tick(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once);
|
||||||
|
|
||||||
|
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
|
||||||
|
ignoreNonGoAwayFrames: false,
|
||||||
|
expectedLastStreamId: 1,
|
||||||
|
Http2ErrorCode.INTERNAL_ERROR,
|
||||||
|
null);
|
||||||
|
|
||||||
|
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
|
||||||
|
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DATA_Sent_TooSlowlyDueToOutputFlowControlOnMultipleStreams_AbortsConnectionAfterAdditiveRateTimeout()
|
||||||
|
{
|
||||||
|
var mockSystemClock = _serviceContext.MockSystemClock;
|
||||||
|
var limits = _serviceContext.ServerOptions.Limits;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
var timeToWriteMaxData = TimeSpan.FromSeconds(_clientSettings.InitialWindowSize / limits.MinResponseDataRate.BytesPerSecond);
|
||||||
|
// Double the timeout for the second stream.
|
||||||
|
timeToWriteMaxData += timeToWriteMaxData;
|
||||||
|
|
||||||
|
// Don't send WINDOW_UPDATE to induce flow-control backpressure
|
||||||
|
mockSystemClock.UtcNow += timeToWriteMaxData + Heartbeat.Interval;
|
||||||
|
_timeoutControl.Tick(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
|
||||||
|
|
||||||
|
mockSystemClock.UtcNow += TimeSpan.FromTicks(1);
|
||||||
|
_timeoutControl.Tick(mockSystemClock.UtcNow);
|
||||||
|
|
||||||
|
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once);
|
||||||
|
|
||||||
|
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
|
||||||
|
ignoreNonGoAwayFrames: false,
|
||||||
|
expectedLastStreamId: 3,
|
||||||
|
Http2ErrorCode.INTERNAL_ERROR,
|
||||||
|
null);
|
||||||
|
|
||||||
|
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
|
||||||
|
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,35 +35,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Server_Http2Only_Cleartext_Success()
|
public Task Server_Http2Only_Cleartext_Success()
|
||||||
{
|
{
|
||||||
// Expect a SETTINGS frame (type 0x4) with default settings
|
// Expect a SETTINGS frame with default settings then a connection-level WINDOW_UPDATE frame.
|
||||||
var expected = new byte[]
|
var expected = new byte[]
|
||||||
{
|
{
|
||||||
0x00, 0x00, 0x12, // Payload Length (6 * settings count)
|
0x00, 0x00, 0x12, // Payload Length (6 * settings count)
|
||||||
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // SETTINGS frame (type 0x04)
|
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // SETTINGS frame (type 0x04)
|
||||||
0x00, 0x03, 0x00, 0x00, 0x00, 0x64, // Connection limit
|
0x00, 0x03, 0x00, 0x00, 0x00, 0x64, // Connection limit (100)
|
||||||
0x00, 0x04, 0x00, 0x01, 0x80, 0x00, // Initial window size
|
0x00, 0x04, 0x00, 0x01, 0x80, 0x00, // Initial stream window size (96 KiB)
|
||||||
0x00, 0x06, 0x00, 0x00, 0x80, 0x00 // Header size limit
|
0x00, 0x06, 0x00, 0x00, 0x80, 0x00, // Header size limit (32 KiB)
|
||||||
};
|
0x00, 0x00, 0x04, // Payload Length (4)
|
||||||
var testContext = new TestServiceContext(LoggerFactory);
|
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, // WINDOW_UPDATE frame (type 0x08)
|
||||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
0x00, 0x01, 0x00, 0x01, // Diff between configured and protocol default (128 KiB - 0XFFFF)
|
||||||
{
|
|
||||||
Protocols = HttpProtocols.Http2
|
|
||||||
};
|
};
|
||||||
|
|
||||||
using (var server = new TestServer(context => Task.CompletedTask, testContext, listenOptions))
|
return TestSuccess(HttpProtocols.Http2,
|
||||||
{
|
Encoding.ASCII.GetString(Http2Connection.ClientPreface),
|
||||||
using (var connection = server.CreateConnection())
|
Encoding.ASCII.GetString(expected));
|
||||||
{
|
|
||||||
await connection.Send(Encoding.ASCII.GetString(Http2Connection.ClientPreface));
|
|
||||||
// Can't use Receive when expecting binary data
|
|
||||||
var actual = new byte[expected.Length];
|
|
||||||
var read = await connection.Stream.ReadAsync(actual, 0, actual.Length);
|
|
||||||
Assert.Equal(expected.Length, read);
|
|
||||||
Assert.Equal(expected, actual);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TestSuccess(HttpProtocols serverProtocols, string request, string expectedResponse)
|
private async Task TestSuccess(HttpProtocols serverProtocols, string request, string expectedResponse)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue